commit 5bb63bacf59b40cd89f993d6ad1495691596f252 Author: Adrian Altner Date: Wed Apr 22 10:55:29 2026 +0200 Initial commit: Astro 6 static blog site - German (default) and English i18n support - Categories and tags - Blog posts with hero images - Dark/light theme switcher - View Transitions removed to fix reload ghost images - Webmentions integration - RSS feeds per locale Co-Authored-By: Claude Sonnet 4.5 diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..cc9ef50 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "astro-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 4321 + } + ] +} diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..b403634 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,63 @@ +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: self-hosted + env: + DEPLOY_DIR: /opt/websites/adrian-altner.de + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync to deploy directory + run: | + sudo rsync -a --delete \ + --exclude='.env' \ + --exclude='.env.production' \ + --exclude='.git/' \ + --exclude='node_modules/' \ + ./ "${DEPLOY_DIR}/" + + - name: Secret check + env: + WEBMENTION_TOKEN: ${{ secrets.WEBMENTION_TOKEN }} + WEBMENTION_APP_TOKEN: ${{ secrets.WEBMENTION_APP_TOKEN }} + run: | + echo "WEBMENTION_TOKEN length: ${#WEBMENTION_TOKEN}" + echo "WEBMENTION_APP_TOKEN length: ${#WEBMENTION_APP_TOKEN}" + + - name: Build image + run: | + cd "${DEPLOY_DIR}" + sudo podman build \ + --no-cache \ + --build-arg WEBMENTION_TOKEN="${{ secrets.WEBMENTION_TOKEN }}" \ + -t localhost/adrian-altner.de:latest . + + - name: Restart service + run: sudo systemctl restart podman-compose@adrian-altner.de.service + + - name: Prune + run: | + sudo podman container prune -f 2>/dev/null || true + sudo podman image prune --external -f 2>/dev/null || true + sudo podman image prune -f 2>/dev/null || true + sudo podman builder prune -af 2>/dev/null || true + + - name: Send webmentions + env: + WEBMENTION_APP_TOKEN: ${{ secrets.WEBMENTION_APP_TOKEN }} + run: | + if [ -z "$WEBMENTION_APP_TOKEN" ]; then + echo "No WEBMENTION_APP_TOKEN — skipping." + exit 0 + fi + for feed in rss.xml en/rss.xml; do + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/${feed}&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d89661 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.local +.env.development +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..56f043d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d642209 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5b6276a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,45 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +- `npm run dev` — local dev server at `localhost:4321` +- `npm run build` — production build to `./dist/` +- `npm run preview` — preview production build +- `npm run astro -- --help` — Astro CLI (e.g. `astro check` for type-checking) + +Node >= 22.12.0 required. + +## Path aliases + +[tsconfig.json](tsconfig.json) defines a single `~/*` → `src/*` alias. Prefer it over relative imports (`~/components/Foo.astro` instead of `../../components/Foo.astro`) in `.astro`, `.ts`, `.js`, `.mjs`, and MDX files. Frontmatter `heroImage` paths and inline markdown `![](…)` image refs stay relative — Astro's image resolver expects those relative to the content file. + +## Architecture + +Astro 6 static site, based on the `blog` starter template. No tests, no linter configured. Site URL: `https://adrian-altner.de`. + +### Internationalisation (de default, en secondary) + +- Astro i18n is configured in [astro.config.mjs](astro.config.mjs) with `defaultLocale: 'de'`, `locales: ['de', 'en']`, `prefixDefaultLocale: false`. German lives at `/`, English at `/en/`. +- Posts are organised per locale under `src/content/posts//…`. Post `id` is `/`; helpers in [src/i18n/posts.ts](src/i18n/posts.ts) (`entryLocale`, `entrySlug`, `getPostsByLocale`, `getCategoriesByLocale`, `getPostsByCategory`, `categoryHref`, `categorySegment`) split and filter them. The content schema in [src/content.config.ts](src/content.config.ts) globs `{de,en}/**/*.{md,mdx}`. Collection name is `posts` — access via `getCollection('posts')` / `CollectionEntry<'posts'>`. +- A second collection `categories` lives under `src/content/categories//*.md` (schema: `name`, optional `description`, optional `translationKey`). Blog posts reference a category via `category: reference('categories')` in the schema; frontmatter values are fully-qualified entry ids, e.g. `category: de/technik` or `category: en/tech`. Resolve with `getEntry(post.data.category)`. +- **Translation linking**: both collections support an optional `translationKey` string. Entries in different locales that represent the same logical content share one key (e.g. `de/technik` and `en/tech` both set `translationKey: tech`). The language switcher resolves this via `findTranslation(entry, target)` ([src/i18n/posts.ts](src/i18n/posts.ts)) to produce the correct target-locale URL. Pages that render a specific content entry should pass it to `Header` as `entry={…}` (see [src/pages/[...slug].astro](src/pages/[...slug].astro), [CategoryDetailPage](src/components/CategoryDetailPage.astro)); the `BlogPost` layout forwards an `entry` prop. When an entry has no translation, the switcher falls back to the other locale's home rather than producing a 404. +- Category routes are localised in the URL: `/kategorie/` (de) and `/en/category/` (en), driven by [src/pages/kategorie/[slug].astro](src/pages/kategorie/[slug].astro) and [src/pages/en/category/[slug].astro](src/pages/en/category/[slug].astro). Both use shared UI components ([CategoryIndexPage](src/components/CategoryIndexPage.astro), [CategoryDetailPage](src/components/CategoryDetailPage.astro)). `categorySegment(locale)` returns the right URL segment. +- Because posts sit two levels deep, hero images and component imports inside MD/MDX use `../../../assets/…` / `../../../components/…` relative paths. +- UI strings and path helpers live in [src/i18n/ui.ts](src/i18n/ui.ts). `t(locale, key)` for translations; `localizePath(path, locale)` prefixes `/en` when needed; `switchLocalePath(pathname, target)` rewrites the current URL to the other locale (used by the header language switcher and hreflang alternates in [BaseHead.astro](src/components/BaseHead.astro)). +- Site titles/descriptions per locale live in [src/consts.ts](src/consts.ts) (`SITE.de`, `SITE.en`). The `SITE[locale]` map is the single source of truth — update when rebranding. +- Pages: German under `src/pages/` (`index.astro`, `about.astro`, `[...slug].astro`, `rss.xml.js`), English mirrors under `src/pages/en/`. The shared home UI lives in [src/components/HomePage.astro](src/components/HomePage.astro); both `index.astro` files are thin wrappers that pass `locale="de"` / `locale="en"`. +- Layouts: [BaseLayout.astro](src/layouts/BaseLayout.astro) is the common skeleton (`` / `` with `BaseHead` / `` with `Header` + `main` + `Footer`). Accepts `title`, `description`, `locale`, optional `image`, optional `entry` (for the language-switch translation lookup), optional `bodyClass`, and a `head` named slot for per-page ``/`` extras. All page templates compose via this layout — don't re-assemble head/header/footer by hand. [Post.astro](src/layouts/Post.astro) wraps `BaseLayout` to add hero image + title block + category line for single posts. `Header`, `BaseHead`, and `FormattedDate` also accept `locale` directly and fall back to `getLocaleFromUrl(Astro.url)`. +- Separate RSS feeds per locale: `/rss.xml` (de) and `/en/rss.xml`. The sitemap integration is configured with `i18n: { defaultLocale: 'de', locales: { de: 'de-DE', en: 'en-US' } }`. + +### Routing and data flow + +- [src/pages/[...slug].astro](src/pages/[...slug].astro) and [src/pages/en/[...slug].astro](src/pages/en/[...slug].astro) generate post pages via `getStaticPaths` + `getPostsByLocale(locale)`, rendering through [BlogPost.astro](src/layouts/BlogPost.astro). +- `@astrojs/sitemap` generates the sitemap index; `` tags are emitted in `BaseHead.astro` for both locales plus `x-default`. +- **Fonts**: Atkinson is loaded as a local font via Astro's `fonts` API in `astro.config.mjs`, exposed as CSS variable `--font-atkinson`. Files in `src/assets/fonts/`. +- **MDX** is enabled via `@astrojs/mdx`; posts can mix Markdown and components. + +## Hero image workflow + +Per user convention: hero image templates live under `src/assets/heros/*`. Workflow uses Maple Mono font, headless Brave to render, then `sharp` to export JPG at 1020×510. diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..06e85f9 --- /dev/null +++ b/Containerfile @@ -0,0 +1,35 @@ +FROM node:22-bookworm-slim AS build + +ARG WEBMENTION_TOKEN="" +ENV WEBMENTION_TOKEN=$WEBMENTION_TOKEN + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + + +FROM node:22-bookworm-slim AS runtime + +WORKDIR /app + +ENV NODE_ENV=production +ENV ASTRO_TELEMETRY_DISABLED=1 +ENV HOST=0.0.0.0 +ENV PORT=4321 + +COPY --from=build --chown=node:node /app/package.json ./package.json +COPY --from=build --chown=node:node /app/package-lock.json ./package-lock.json +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/dist ./dist + +USER node + +EXPOSE 4321 + +CMD ["node", "dist/server/entry.mjs"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..3883e37 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Astro Starter Kit: Blog + +```sh +npm create astro@latest -- --template blog +``` + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +Features: + +- ✅ Minimal styling (make it your own!) +- ✅ 100/100 Lighthouse performance +- ✅ SEO-friendly with canonical URLs and Open Graph data +- ✅ Sitemap support +- ✅ RSS Feed support +- ✅ Markdown & MDX support + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +├── public/ +├── src/ +│   ├── assets/ +│   ├── components/ +│   ├── content/ +│   ├── layouts/ +│   └── pages/ +├── astro.config.mjs +├── README.md +├── package.json +└── tsconfig.json +``` + +Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + +There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + +The `src/content/` directory contains "collections" of related Markdown and MDX documents. Use `getCollection()` to retrieve posts from `src/content/blog/`, and type-check your frontmatter using an optional schema. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more. + +Any static assets, like images, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). + +## Credit + +This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/). diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..27c9723 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,75 @@ +// @ts-check + +import mdx from '@astrojs/mdx'; +import sitemap from '@astrojs/sitemap'; +import { defineConfig, fontProviders } from 'astro/config'; +import { loadEnv } from 'vite'; + +import node from '@astrojs/node'; + +const envMode = process.env.NODE_ENV === 'production' ? 'production' : 'development'; +const envVars = loadEnv(envMode, process.cwd(), ''); +const WEBMENTION_TOKEN = envVars.WEBMENTION_TOKEN || process.env.WEBMENTION_TOKEN || ''; + +// https://astro.build/config +export default defineConfig({ + site: 'https://adrian-altner.de', + + vite: { + define: { + 'globalThis.__WEBMENTION_TOKEN__': JSON.stringify(WEBMENTION_TOKEN), + }, + }, + + devToolbar: { + enabled: false, + }, + + i18n: { + defaultLocale: 'de', + locales: ['de', 'en'], + routing: { + prefixDefaultLocale: false, + redirectToDefaultLocale: false, + }, + }, + + integrations: [ + mdx(), + sitemap({ + i18n: { + defaultLocale: 'de', + locales: { de: 'de-DE', en: 'en-US' }, + }, + }), + ], + + fonts: [ + { + provider: fontProviders.local(), + name: 'Atkinson', + cssVariable: '--font-atkinson', + fallbacks: ['sans-serif'], + options: { + variants: [ + { + src: ['./src/assets/fonts/atkinson-regular.woff'], + weight: 400, + style: 'normal', + display: 'swap', + }, + { + src: ['./src/assets/fonts/atkinson-bold.woff'], + weight: 700, + style: 'normal', + display: 'swap', + }, + ], + }, + }, + ], + + adapter: node({ + mode: 'standalone', + }), +}); \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..1056761 --- /dev/null +++ b/compose.yml @@ -0,0 +1,13 @@ +name: adrian-altner-de + +services: + website: + image: localhost/adrian-altner.de:latest + container_name: adrian-altner.de + ports: + - "4321:4321" + environment: + NODE_ENV: production + HOST: 0.0.0.0 + PORT: 4321 + restart: unless-stopped diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dfbcc34 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5928 @@ +{ + "name": "adrian-altner.de", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "adrian-altner.de", + "version": "0.0.1", + "dependencies": { + "@astrojs/mdx": "^5.0.3", + "@astrojs/node": "^10.0.5", + "@astrojs/rss": "^4.0.18", + "@astrojs/sitemap": "^3.7.2", + "astro": "^6.1.8", + "sharp": "^0.34.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/compiler": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-3.0.1.tgz", + "integrity": "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.8.0.tgz", + "integrity": "sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==", + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.3" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.0.tgz", + "integrity": "sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.8.0", + "@astrojs/prism": "4.0.1", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.3.tgz", + "integrity": "sha512-zv/OlM5sZZvyjHqJjR3FjJvoCgbxdqj3t4jO/gSEUNcck3BjdtMgNQw8UgPfAGe4yySdG4vjZ3OC5wUxhu7ckg==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "7.1.0", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "es-module-lexer": "^2.0.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/node": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-10.0.5.tgz", + "integrity": "sha512-rgZiU9nD7zmM3fdmOVuobcNHAyXWx2HXXDLTuxjVCTQ+QmHmL5zkZZhNIL5NjlQtDRAU1i5fVaXp7nAKdET30w==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.8.0", + "send": "^1.2.1", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", + "integrity": "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/rss": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.18.tgz", + "integrity": "sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.5.7", + "piccolore": "^0.1.3", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.2.tgz", + "integrity": "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.1.tgz", + "integrity": "sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.2.0.tgz", + "integrity": "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.1.3", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.2.0.tgz", + "integrity": "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.2.0", + "fast-string-width": "^1.1.0", + "fast-wrap-ansi": "^0.1.3", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "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.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "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.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "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.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "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.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "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-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "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-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "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.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "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.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "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.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "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.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "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-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "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/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "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==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.8.tgz", + "integrity": "sha512-6fT9M12U3fpi13DiPavNKDIoBflASTSxmKTEe+zXhWtlebQuOqfOnIrMWyRmlXp+mgDsojmw+fVFG9LUTzKSog==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^3.0.1", + "@astrojs/internal-helpers": "0.8.0", + "@astrojs/markdown-remark": "7.1.0", + "@astrojs/telemetry": "3.3.1", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.6.3", + "diff": "^8.0.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.4", + "vfile": "^6.0.3", + "vite": "^7.3.1", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", + "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "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==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz", + "integrity": "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-1.1.0.tgz", + "integrity": "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^1.2.0" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.1.6.tgz", + "integrity": "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^1.1.0" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz", + "integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "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-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.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-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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "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-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.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": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "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": "^7.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-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "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": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^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-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.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-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "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-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==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "license": "MIT", + "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.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "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-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "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.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "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-stringify-position": "^4.0.0", + "vfile-message": "^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==", + "license": "MIT", + "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-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "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.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "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-classify-character": "^2.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-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==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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-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==", + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "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.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "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==", + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "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-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.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-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.2.tgz", + "integrity": "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^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-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "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==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "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-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "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-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "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-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.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==", + "license": "MIT", + "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.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "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==", + "license": "MIT", + "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.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "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/tinyclip": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", + "integrity": "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "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/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "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==", + "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-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "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.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d3a26f8 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "adrian-altner.de", + "type": "module", + "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/mdx": "^5.0.3", + "@astrojs/node": "^10.0.5", + "@astrojs/rss": "^4.0.18", + "@astrojs/sitemap": "^3.7.2", + "astro": "^6.1.8", + "sharp": "^0.34.3" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..7f48a94 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/scripts/VISION.md b/scripts/VISION.md new file mode 100644 index 0000000..8bd3191 --- /dev/null +++ b/scripts/VISION.md @@ -0,0 +1,76 @@ +# Vision Script + +Generiert Metadaten-Sidecars (JSON) fuer Foto-Kollektionen mithilfe von EXIF-Daten und einer Vision-AI (Anthropic oder OpenAI). + +## Voraussetzungen + +- `exiftool` installiert (`brew install exiftool`) +- `ANTHROPIC_API_KEY` oder `OPENAI_API_KEY` in `.env.local` gesetzt (je nach Provider) + +## Aufruf + +```bash +pnpm run vision [optionen] [verzeichnis] +``` + +Ohne Verzeichnis wird der Standard `content/fotos` verwendet. + +## Optionen + +| Option | Beschreibung | +|---|---| +| `--provider=anthropic\|openai` | Vision-API Provider (Standard: `anthropic`). Anthropic nutzt `claude-opus-4-6`, OpenAI nutzt `gpt-4o-mini`. | +| `--refresh` | Alle Sidecars neu generieren (EXIF + AI). Ueberschreibt vorhandene Dateien. | +| `--exif-only` | Nur EXIF-Daten in bestehenden Sidecars aktualisieren. AI-Felder (Titel, Alt, Tags) bleiben erhalten. | +| `--concurrency=N` | Anzahl paralleler Vision-API-Anfragen (Standard: 2) | +| `--retries=N` | Maximale Wiederholungsversuche bei Rate-Limits (Standard: 8) | +| `--backoff-ms=N` | Basis-Wartezeit in ms fuer exponentielles Backoff (Standard: 1500) | + +## Umgebungsvariablen + +Alternativ zu den CLI-Optionen koennen diese Werte auch per Umgebungsvariable gesetzt werden: + +| Variable | Entspricht | +|---|---| +| `VISION_PROVIDER` | `--provider` | +| `VISION_CONCURRENCY` | `--concurrency` | +| `VISION_MAX_RETRIES` | `--retries` | +| `VISION_BASE_BACKOFF_MS` | `--backoff-ms` | + +CLI-Optionen haben Vorrang vor Umgebungsvariablen. + +## Beispiele + +```bash +# Neue Bilder ohne Sidecar verarbeiten (Anthropic) +pnpm run vision + +# Mit OpenAI statt Anthropic +pnpm run vision --provider=openai + +# Alle Sidecars in einem bestimmten Ordner neu generieren +pnpm run vision --refresh --provider=openai content/fotos/kollektionen/reisen/asien/thailand + +# Nur EXIF-Daten aktualisieren (z.B. nach erneutem Lightroom-Export) +pnpm run vision --exif-only + +# Mit hoeherer Parallelitaet +pnpm run vision --refresh --concurrency=4 +``` + +## Ausgabe + +Pro Bild wird eine JSON-Sidecar-Datei mit folgendem Inhalt erstellt: + +- `title` - 5 Titelvorschlaege (deutsch, via AI) +- `alt` - Bildbeschreibung / Alt-Text (deutsch, via AI) +- `tags` - 5 thematische Tags (deutsch, via AI) +- `date` - Aufnahmedatum (aus EXIF) +- `location` - GPS-Koordinaten (aus EXIF, falls vorhanden) +- `locationName` - Aufgeloester Ortsname via Nominatim (falls GPS vorhanden) +- `exif` - Kamera, Objektiv, Blende, ISO, Brennweite, Belichtungszeit + + +# Fix +bei OPENAI rate-limit mit einem gerigem Tier-Level +pnpm run vision --refresh --provider=openai --concurrency=1 content/fotos/kollektionen/reisen/asien/malaysia diff --git a/scripts/copy-sw.js b/scripts/copy-sw.js new file mode 100644 index 0000000..a656a86 --- /dev/null +++ b/scripts/copy-sw.js @@ -0,0 +1,17 @@ +// Copies sw.js + workbox-*.js from dist/server/ to dist/client/ after build. +// @astrojs/node standalone only serves static files from dist/client/, but +// @vite-pwa/astro generates the service worker into dist/server/ during the +// SSR Vite build pass. +import { copyFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; + +const serverDir = "dist/server"; +const clientDir = "dist/client"; + +const files = await readdir(serverDir).catch(() => []); +for (const file of files) { + if (file === "sw.js" || file.startsWith("workbox-")) { + await copyFile(join(serverDir, file), join(clientDir, file)); + console.log(`[copy-sw] ${file} → dist/client/`); + } +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..c220008 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" +REMOTE_BASE='/opt/websites/adrian-altner.de' +REMOTE_URL='ssh://git@git.altner.cloud:2222/adrian/adrian-altner.de.git' +GIT_HOST='git.altner.cloud' +GIT_PORT='2222' + +# --- 1. Pull latest from repo --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + mkdir -p ~/.ssh && touch ~/.ssh/known_hosts && chmod 600 ~/.ssh/known_hosts + ssh-keygen -F '[$GIT_HOST]:$GIT_PORT' >/dev/null || ssh-keyscan -p '$GIT_PORT' '$GIT_HOST' >> ~/.ssh/known_hosts + git remote set-url origin '$REMOTE_URL' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production +" + +# --- 2. Build + deploy --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + sudo podman build -t localhost/adrian-altner.de:latest . + sudo systemctl restart podman-compose@adrian-altner.de.service + sudo podman container prune -f 2>/dev/null || true + sudo podman image prune --external -f 2>/dev/null || true + sudo podman image prune -f 2>/dev/null || true + sudo podman builder prune -af 2>/dev/null || true +" + +echo "Deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions via webmention.app..." + for feed in rss.xml en/rss.xml; do + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/${feed}&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + done + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/fetch-favicons.mjs b/scripts/fetch-favicons.mjs new file mode 100644 index 0000000..9561bf9 --- /dev/null +++ b/scripts/fetch-favicons.mjs @@ -0,0 +1,74 @@ +/** + * Fetches favicons for all linked domains and saves them locally to public/favicons/. + * Run before astro build so favicons are served statically instead of via Google S2. + * Idempotent: skips domains that already have a cached favicon. + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const root = join(__dirname, ".."); +const linksPath = join(root, "src/content/links/links.json"); +const outDir = join(root, "public/favicons"); + +function getDomain(url) { + try { + return new URL(url).hostname.replace(/^www\./, ""); + } catch { + return null; + } +} + +function faviconPath(domain) { + return join(outDir, `${domain}.png`); +} + +async function fetchFavicon(domain) { + const url = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const buf = await res.arrayBuffer(); + return Buffer.from(buf); +} + +async function main() { + const links = JSON.parse(readFileSync(linksPath, "utf-8")); + + const domains = [ + ...new Set(links.map((l) => getDomain(l.url)).filter(Boolean)), + ]; + + if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); + + let fetched = 0; + let skipped = 0; + + await Promise.all( + domains.map(async (domain) => { + const dest = faviconPath(domain); + if (existsSync(dest)) { + skipped++; + return; + } + try { + const data = await fetchFavicon(domain); + writeFileSync(dest, data); + fetched++; + console.log(` ✓ ${domain}`); + } catch (err) { + console.warn(` ✗ ${domain}: ${err.message}`); + } + }), + ); + + console.log( + `Favicons: ${fetched} fetched, ${skipped} cached, ${domains.length} total`, + ); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/metadata.ts b/scripts/metadata.ts new file mode 100644 index 0000000..9a25f3f --- /dev/null +++ b/scripts/metadata.ts @@ -0,0 +1,424 @@ +#!/usr/bin/env -S node --experimental-strip-types + +import { writeFile } from "node:fs/promises"; +import { basename, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { consola } from "consola"; +import sharp from "sharp"; +import { + getImagesMissingMetadata, + getMetadataPathForImage, + getPhotoAbsolutePath, + getPhotoDirectories, + PHOTOS_DIRECTORY, +} from "../src/lib/photo-albums.ts"; + +const PHOTOS_DIR = PHOTOS_DIRECTORY; + +// ─── IPTC parser ──────────────────────────────────────────────────────────── + +interface IptcFields { + title?: string; + caption?: string; + keywords?: string[]; + dateCreated?: string; + timeCreated?: string; +} + +function parseIptc(buf: Buffer): IptcFields { + const fields: IptcFields = {}; + let i = 0; + + while (i < buf.length - 4) { + if (buf[i] !== 0x1c) { + i++; + continue; + } + + const record = buf[i + 1]; + const dataset = buf[i + 2]; + const len = buf.readUInt16BE(i + 3); + const value = buf.subarray(i + 5, i + 5 + len).toString("utf8"); + i += 5 + len; + + if (record !== 2) continue; + + switch (dataset) { + case 5: + fields.title = value; + break; + case 25: + fields.keywords ??= []; + fields.keywords.push(value); + break; + case 55: + fields.dateCreated = value; + break; + case 60: + fields.timeCreated = value; + break; + case 120: + fields.caption = value; + break; + } + } + + return fields; +} + +// ─── XMP parser ───────────────────────────────────────────────────────────── + +interface XmpFields { + title?: string; + description?: string; + keywords?: string[]; + lens?: string | undefined; + createDate?: string | undefined; +} + +function extractRdfLiValues(xml: string, tagName: string): string[] { + const re = new RegExp(`<${tagName}[^>]*>[\\s\\S]*?<\\/${tagName}>`, "i"); + const match = xml.match(re); + if (!match) return []; + + const liRe = /]*>([^<]*)<\/rdf:li>/gi; + const values: string[] = []; + for (let m = liRe.exec(match[0]); m !== null; m = liRe.exec(match[0])) { + if (m[1]?.trim()) values.push(m[1].trim()); + } + return values; +} + +function extractXmpAttr(xml: string, attr: string): string | undefined { + const re = new RegExp(`${attr}="([^"]*)"`, "i"); + return xml.match(re)?.[1] ?? undefined; +} + +function parseXmp(buf: Buffer): XmpFields { + const xml = buf.toString("utf8"); + const fields: XmpFields = {}; + + const titles = extractRdfLiValues(xml, "dc:title"); + if (titles[0]) fields.title = titles[0]; + + const descriptions = extractRdfLiValues(xml, "dc:description"); + if (descriptions[0]) fields.description = descriptions[0]; + + const subjects = extractRdfLiValues(xml, "dc:subject"); + if (subjects.length > 0) fields.keywords = subjects; + + fields.lens = extractXmpAttr(xml, "aux:Lens"); + fields.createDate = extractXmpAttr(xml, "xmp:CreateDate"); + + return fields; +} + +// ─── EXIF parser (minimal TIFF IFD0 + SubIFD) ────────────────────────────── + +interface ExifFields { + model?: string; + lensModel?: string; + fNumber?: number; + focalLength?: string; + exposureTime?: string; + iso?: number; + dateTimeOriginal?: string; + gpsLatitude?: string; + gpsLongitude?: string; + gpsLatitudeRef?: string; + gpsLongitudeRef?: string; +} + +function parseExifBuffer(buf: Buffer): ExifFields { + // Skip "Exif\0\0" header if present + let offset = 0; + if ( + buf[0] === 0x45 && + buf[1] === 0x78 && + buf[2] === 0x69 && + buf[3] === 0x66 + ) { + offset = 6; + } + + const isLE = buf[offset] === 0x49; // "II" = little endian + const read16 = isLE + ? (o: number) => buf.readUInt16LE(offset + o) + : (o: number) => buf.readUInt16BE(offset + o); + const read32 = isLE + ? (o: number) => buf.readUInt32LE(offset + o) + : (o: number) => buf.readUInt32BE(offset + o); + + const readRational = (o: number): number => { + const num = read32(o); + const den = read32(o + 4); + return den === 0 ? 0 : num / den; + }; + + const readString = (o: number, len: number): string => { + return buf + .subarray(offset + o, offset + o + len) + .toString("ascii") + .replace(/\0+$/, ""); + }; + + const fields: ExifFields = {}; + + const parseIfd = (ifdOffset: number, parseGps = false) => { + if (ifdOffset + 2 > buf.length - offset) return; + const count = read16(ifdOffset); + + for (let i = 0; i < count; i++) { + const entryOffset = ifdOffset + 2 + i * 12; + if (entryOffset + 12 > buf.length - offset) break; + + const tag = read16(entryOffset); + const type = read16(entryOffset + 2); + const numValues = read32(entryOffset + 4); + const valueOffset = read32(entryOffset + 8); + + // For values that fit in 4 bytes, data is inline at entryOffset+8 + const dataOffset = + type === 2 && numValues <= 4 + ? entryOffset + 8 + : type === 5 || numValues > 4 + ? valueOffset + : entryOffset + 8; + + if (parseGps) { + switch (tag) { + case 1: // GPSLatitudeRef + fields.gpsLatitudeRef = readString(entryOffset + 8, 2); + break; + case 2: // GPSLatitude + if (dataOffset + 24 <= buf.length - offset) { + const d = readRational(dataOffset); + const m = readRational(dataOffset + 8); + const s = readRational(dataOffset + 16); + fields.gpsLatitude = `${d} deg ${Math.floor(m)}' ${s.toFixed(2)}"`; + } + break; + case 3: // GPSLongitudeRef + fields.gpsLongitudeRef = readString(entryOffset + 8, 2); + break; + case 4: // GPSLongitude + if (dataOffset + 24 <= buf.length - offset) { + const d = readRational(dataOffset); + const m = readRational(dataOffset + 8); + const s = readRational(dataOffset + 16); + fields.gpsLongitude = `${d} deg ${Math.floor(m)}' ${s.toFixed(2)}"`; + } + break; + } + continue; + } + + switch (tag) { + case 0x0110: // Model + if (dataOffset + numValues <= buf.length - offset) { + fields.model = readString(dataOffset, numValues); + } + break; + case 0x8769: // ExifIFD pointer + parseIfd(valueOffset); + break; + case 0x8825: // GPS IFD pointer + parseIfd(valueOffset, true); + break; + case 0x829a: // ExposureTime + if (dataOffset + 8 <= buf.length - offset) { + const num = read32(dataOffset); + const den = read32(dataOffset + 4); + fields.exposureTime = + den > num ? `1/${Math.round(den / num)}` : `${num / den}`; + } + break; + case 0x829d: // FNumber + if (dataOffset + 8 <= buf.length - offset) { + fields.fNumber = readRational(dataOffset); + } + break; + case 0x8827: // ISO + fields.iso = type === 3 ? read16(entryOffset + 8) : valueOffset; + break; + case 0x9003: // DateTimeOriginal + if (dataOffset + numValues <= buf.length - offset) { + fields.dateTimeOriginal = readString(dataOffset, numValues); + } + break; + case 0x920a: // FocalLength + if (dataOffset + 8 <= buf.length - offset) { + const fl = readRational(dataOffset); + fields.focalLength = fl.toFixed(1).replace(/\.0$/, ""); + } + break; + case 0xa434: // LensModel + if (dataOffset + numValues <= buf.length - offset) { + fields.lensModel = readString(dataOffset, numValues); + } + break; + } + } + }; + + const ifdOffset = read32(4); + parseIfd(ifdOffset); + + return fields; +} + +// ─── Merged metadata ──────────────────────────────────────────────────────── + +interface ImageMetadata { + id: string; + title: string[]; + image: string; + alt: string; + location: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +} + +function formatGpsLocation(exif: ExifFields): string { + if (!exif.gpsLatitude || !exif.gpsLongitude) return ""; + + const latRef = exif.gpsLatitudeRef ?? "N"; + const lonRef = exif.gpsLongitudeRef ?? "E"; + return `${exif.gpsLatitude} ${latRef}, ${exif.gpsLongitude} ${lonRef}`; +} + +function formatDate(raw: string | undefined): string { + if (!raw) return ""; + + // Handle "YYYY:MM:DD HH:MM:SS" or "YYYYMMDD" or "YYYY-MM-DDTHH:MM:SS" + if (/^\d{8}$/.test(raw)) { + return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`; + } + + const [datePart] = raw.split(/[T ]/); + if (!datePart) return ""; + return datePart.replaceAll(":", "-"); +} + +async function extractMetadata(imagePath: string): Promise { + const meta = await sharp(imagePath).metadata(); + const fileName = basename(imagePath); + + const iptc = meta.iptc ? parseIptc(meta.iptc) : ({} as IptcFields); + const xmp = meta.xmp ? parseXmp(meta.xmp) : ({} as XmpFields); + const exif = meta.exif ? parseExifBuffer(meta.exif) : ({} as ExifFields); + + const title = iptc.title || xmp.title || ""; + const caption = iptc.caption || xmp.description || ""; + const keywords = iptc.keywords ?? xmp.keywords ?? []; + const date = formatDate( + exif.dateTimeOriginal ?? xmp.createDate ?? iptc.dateCreated, + ); + + if (!title && !caption) { + consola.warn(`No title or caption found in ${fileName}`); + } + + return { + id: fileName.replace(/\.jpg$/i, ""), + title: title ? [title] : [], + image: `./${fileName}`, + alt: caption, + location: formatGpsLocation(exif), + date, + tags: keywords, + exif: { + camera: exif.model ?? "", + lens: exif.lensModel || xmp.lens || "", + aperture: exif.fNumber?.toString() ?? "", + iso: exif.iso?.toString() ?? "", + focal_length: exif.focalLength?.replace(/ mm$/, "") ?? "", + shutter_speed: exif.exposureTime ?? "", + }, + }; +} + +// ─── CLI ──────────────────────────────────────────────────────────────────── + +interface CliOptions { + refresh: boolean; + photosDirectory: string; +} + +function parseCliOptions(argv: string[]): CliOptions { + const nonFlagArgs = argv.filter((arg) => !arg.startsWith("--")); + return { + refresh: argv.includes("--refresh"), + photosDirectory: resolve(nonFlagArgs[0] ?? PHOTOS_DIR), + }; +} + +async function getImagesToProcess( + photosDirectory: string, + options: Pick, +): Promise { + const relativeImagePaths = options.refresh + ? (await getPhotoDirectories(photosDirectory)).flatMap((d) => d.imagePaths) + : await getImagesMissingMetadata(photosDirectory); + + consola.info( + options.refresh + ? `Refreshing ${relativeImagePaths.length} image(s)` + : `Found ${relativeImagePaths.length} image(s) without metadata`, + ); + + return relativeImagePaths.map((p) => + getPhotoAbsolutePath(p, photosDirectory), + ); +} + +async function main() { + consola.start("Checking for images to process..."); + const opts = parseCliOptions(process.argv.slice(2)); + + const images = await getImagesToProcess(opts.photosDirectory, opts); + + if (images.length === 0) { + consola.success( + opts.refresh + ? "No images found to refresh." + : "No images require metadata.", + ); + return; + } + + for (let i = 0; i < images.length; i++) { + const imagePath = images[i] as string; + const rel = relative(process.cwd(), imagePath); + consola.info(`Processing ${i + 1}/${images.length}: ${rel}`); + + const metadata = await extractMetadata(imagePath); + const relativeImagePath = relative(opts.photosDirectory, imagePath); + const jsonPath = getMetadataPathForImage( + relativeImagePath, + opts.photosDirectory, + ); + + await writeFile(jsonPath, JSON.stringify(metadata, null, 2)); + consola.info(`Wrote ${relative(process.cwd(), jsonPath)}`); + } + + consola.success(`Processed ${images.length} image(s).`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + try { + await main(); + } catch (error) { + consola.error(error); + process.exit(1); + } +} diff --git a/scripts/new-note-mdx-prompt.sh b/scripts/new-note-mdx-prompt.sh new file mode 100755 index 0000000..79d96b3 --- /dev/null +++ b/scripts/new-note-mdx-prompt.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Standalone wrapper für Obsidian Script Runner — neue Note mit Cover-Bild (MDX) +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +TITLE=$(osascript \ + -e 'Tell application "System Events" to display dialog "Note title (with cover):" default answer ""' \ + -e 'text returned of result' 2>/dev/null) || exit 0 + +if [[ -z "$TITLE" ]]; then exit 0; fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%d) +DIR="$VAULT/content/notes/$DATE_FOLDER" +FILE="$DIR/$SLUG.mdx" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + osascript -e "display notification \"File already exists: $SLUG.mdx\" with title \"New Note\"" 2>/dev/null || true + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +cover: "./$SLUG.jpg" +coverAlt: "" +tags: + - +draft: false +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/new-note-prompt.sh b/scripts/new-note-prompt.sh new file mode 100755 index 0000000..abb1d5f --- /dev/null +++ b/scripts/new-note-prompt.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Standalone wrapper für Obsidian Script Runner — neue Note anlegen +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +TITLE=$(osascript \ + -e 'Tell application "System Events" to display dialog "Note title:" default answer ""' \ + -e 'text returned of result' 2>/dev/null) || exit 0 + +if [[ -z "$TITLE" ]]; then exit 0; fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%d) +DIR="$VAULT/content/notes/$DATE_FOLDER" +FILE="$DIR/$SLUG.md" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + osascript -e "display notification \"File already exists: $SLUG.md\" with title \"New Note\"" 2>/dev/null || true + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +tags: + - +draft: false +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/new-note.sh b/scripts/new-note.sh new file mode 100755 index 0000000..cb9d271 --- /dev/null +++ b/scripts/new-note.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Usage: new-note.sh "Note Title" [--mdx] +# Creates a new note in the Obsidian vault with correct frontmatter and folder structure. +# Use --mdx for notes that need a cover image or custom components (creates .mdx file). +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +if [[ -z "${1:-}" ]]; then + echo "Usage: new-note.sh \"Note Title\" [--mdx]" >&2 + exit 1 +fi + +TITLE="$1" +MDX=false +if [[ "${2:-}" == "--mdx" ]]; then + MDX=true +fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%d) +DIR="$VAULT/content/notes/$DATE_FOLDER" + +if $MDX; then + EXT="mdx" +else + EXT="md" +fi + +FILE="$DIR/$SLUG.$EXT" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + echo "File already exists: $FILE" >&2 + exit 1 +fi + +if $MDX; then + cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +cover: "./$SLUG.jpg" +coverAlt: "" +tags: + - +draft: false +syndication: +--- +EOF +else + cat > "$FILE" << EOF +--- +title: "$TITLE" +publishDate: $PUBLISH_DATE +description: "" +tags: + - +draft: false +syndication: +--- +EOF +fi + +echo "Created: $FILE" diff --git a/scripts/new-post-prompt.sh b/scripts/new-post-prompt.sh new file mode 100755 index 0000000..69ad081 --- /dev/null +++ b/scripts/new-post-prompt.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Standalone wrapper für Obsidian Script Runner — neuen Blog-Post anlegen +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +TITLE=$(osascript \ + -e 'Tell application "System Events" to display dialog "Post title:" default answer ""' \ + -e 'text returned of result' 2>/dev/null) || exit 0 + +if [[ -z "$TITLE" ]]; then exit 0; fi + +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%dT%H:%M:%S%z) +DIR="$VAULT/content/blog/posts/$DATE_FOLDER" +FILE="$DIR/$SLUG.md" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + osascript -e "display notification \"File already exists: $SLUG.md\" with title \"New Post\"" 2>/dev/null || true + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +description: "" +publishDate: $PUBLISH_DATE +tags: + - +draft: true +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/new-post.sh b/scripts/new-post.sh new file mode 100755 index 0000000..21e393d --- /dev/null +++ b/scripts/new-post.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Usage: new-post.sh "Post Title" +# Creates a new blog post in the Obsidian vault with correct frontmatter and folder structure. +set -euo pipefail + +VAULT='/Users/adrian/Obsidian/Web/adrian-altner-com' + +if [[ -z "${1:-}" ]]; then + echo "Usage: new-post.sh \"Post Title\"" >&2 + exit 1 +fi + +TITLE="$1" +SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ \+/-/g' | sed 's/^-\+//;s/-\+$//') +DATE_FOLDER=$(date +%Y/%m/%d) +PUBLISH_DATE=$(date +%Y-%m-%dT%H:%M:%S%z) +DIR="$VAULT/content/blog/posts/$DATE_FOLDER" +FILE="$DIR/$SLUG.md" + +mkdir -p "$DIR" + +if [[ -f "$FILE" ]]; then + echo "File already exists: $FILE" >&2 + exit 1 +fi + +cat > "$FILE" << EOF +--- +title: "$TITLE" +description: "" +publishDate: $PUBLISH_DATE +tags: + - +draft: true +syndication: +--- +EOF + +echo "Created: $FILE" diff --git a/scripts/publish-all.sh b/scripts/publish-all.sh new file mode 100755 index 0000000..ec42d4f --- /dev/null +++ b/scripts/publish-all.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +VAULT_CONTENT='/Users/adrian/Library/Mobile Documents/iCloud~md~obsidian/Documents/03 Bereiche/Webseite/adrian-altner-de/content' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/adrian-altner.de' +REMOTE_CONTENT="${REMOTE_BASE}/src/content" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_CONTENT' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.png' \ + --include='*.PNG' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.json' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_CONTENT/" "$VPS:$REMOTE_CONTENT/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + sudo podman build -t localhost/adrian-altner.de:latest . + sudo systemctl restart podman-compose@adrian-altner.de.service + sudo podman container prune -f 2>/dev/null || true + sudo podman image prune --external -f 2>/dev/null || true + sudo podman image prune -f 2>/dev/null || true + sudo podman builder prune -af 2>/dev/null || true +" + +echo "Redeploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions via webmention.app..." + for feed in rss/blog.xml rss/fotos.xml; do + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/${feed}&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + done + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi \ No newline at end of file diff --git a/scripts/publish-blog.sh b/scripts/publish-blog.sh new file mode 100755 index 0000000..f79811e --- /dev/null +++ b/scripts/publish-blog.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Usage: publish-blog.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_BLOG='/Users/adrian/Obsidian/Web/adrian-altner-com/content/blog' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.de' +REMOTE_BLOG="${REMOTE_BASE}/src/content/blog" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_BLOG' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.png' \ + --include='*.PNG' \ + --include='*.webp' \ + --include='*.gif' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_BLOG/" "$VPS:$REMOTE_BLOG/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + + podman-compose -f compose.yml up --build -d --force-recreate + + podman image prune -af + podman builder prune -af || true +" + +echo "Blog deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/rss/blog.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-links.sh b/scripts/publish-links.sh new file mode 100755 index 0000000..c216f8e --- /dev/null +++ b/scripts/publish-links.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Usage: publish-links.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_LINKS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/links' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.de' +REMOTE_LINKS="${REMOTE_BASE}/src/content/links" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_LINKS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_LINKS/" "$VPS:$REMOTE_LINKS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Links deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/rss/links.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-notes.sh b/scripts/publish-notes.sh new file mode 100755 index 0000000..dd21b4a --- /dev/null +++ b/scripts/publish-notes.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Usage: publish-notes.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_NOTES='/Users/adrian/Obsidian/Web/adrian-altner-com/content/notes' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.de' +REMOTE_NOTES="${REMOTE_BASE}/src/content/notes" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_NOTES' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_NOTES/" "$VPS:$REMOTE_NOTES/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + + podman-compose -f compose.yml up --build -d --force-recreate + + podman image prune -af + podman builder prune -af +" + +echo "Notes deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/rss/notes.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-photos.sh b/scripts/publish-photos.sh new file mode 100755 index 0000000..c01bb8e --- /dev/null +++ b/scripts/publish-photos.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Usage: publish-photos.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_PHOTOS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/photos' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.de' +REMOTE_PHOTOS="${REMOTE_BASE}/src/content/photos" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_PHOTOS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.json' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_PHOTOS/" "$VPS:$REMOTE_PHOTOS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Photos deploy done via $VPS (branch: $REMOTE_BRANCH)." + +# --- 3. Webmentions --- +WEBMENTION_APP_TOKEN="$(ssh "$VPS" "grep '^WEBMENTION_APP_TOKEN=' '$REMOTE_BASE/.env.production' | cut -d= -f2-" 2>/dev/null || true)" +if [[ -n "$WEBMENTION_APP_TOKEN" ]]; then + echo "Sending webmentions..." + curl -s -X POST "https://webmention.app/check?url=https://adrian-altner.de/rss/photos.xml&token=${WEBMENTION_APP_TOKEN}" \ + | grep -o '"status":"[^"]*"' || true + echo "Webmentions triggered." +else + echo "No WEBMENTION_APP_TOKEN in .env.production — skipping webmentions." +fi diff --git a/scripts/publish-projects.sh b/scripts/publish-projects.sh new file mode 100755 index 0000000..4694b76 --- /dev/null +++ b/scripts/publish-projects.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Usage: publish-projects.sh [vps-host] [branch] +# Can be called from any directory — no dependency on the repo being the working dir. +set -euo pipefail + +VAULT_PROJECTS='/Users/adrian/Obsidian/Web/adrian-altner-com/content/projects' +VPS="${1:-hetzner}" +REMOTE_BRANCH="${2:-main}" + +REMOTE_BASE='/opt/websites/www.adrian-altner.de' +REMOTE_PROJECTS="${REMOTE_BASE}/src/content/projects" + +# --- 1. Sync vault to VPS --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + git fetch --prune origin '$REMOTE_BRANCH' + git checkout '$REMOTE_BRANCH' + git reset --hard 'origin/$REMOTE_BRANCH' + git clean -fd -e .env -e .env.production + mkdir -p '$REMOTE_PROJECTS' +" + +rsync -az --delete \ + --include='*/' \ + --include='*.md' \ + --include='*.mdx' \ + --include='*.jpg' \ + --include='*.jpeg' \ + --include='*.JPG' \ + --include='*.JPEG' \ + --include='*.png' \ + --include='*.PNG' \ + --include='*.webp' \ + --include='*.gif' \ + --exclude='.DS_Store' \ + --exclude='*' \ + "$VAULT_PROJECTS/" "$VPS:$REMOTE_PROJECTS/" + +# --- 2. Build + cleanup --- +ssh "$VPS" " + set -euo pipefail + cd '$REMOTE_BASE' + podman-compose -f compose.yml up --build -d --force-recreate + podman image prune -af + podman builder prune -af +" + +echo "Projects deploy done via $VPS (branch: $REMOTE_BRANCH)." diff --git a/scripts/squash-history.sh b/scripts/squash-history.sh new file mode 100755 index 0000000..d5c5264 --- /dev/null +++ b/scripts/squash-history.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# squash-history.sh — Replaces entire git history with a single "init" commit. +# WARNING: Destructive and irreversible. Force-pushes to remote. + +set -euo pipefail + +COMMIT_MSG="${1:-init}" +REMOTE="${2:-origin}" +BRANCH="main" +TEMP="temp-squash-$$" + +echo "⚠️ This will destroy all git history and force-push to $REMOTE/$BRANCH." +read -r -p "Continue? [y/N] " confirm +[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } + +git checkout --orphan "$TEMP" +git add -A +git commit -m "$COMMIT_MSG" +git branch -D "$BRANCH" +git branch -m "$TEMP" "$BRANCH" +git push --force "$REMOTE" "$BRANCH" + +echo "Done. $(git log --oneline)" diff --git a/scripts/vision.spec.ts b/scripts/vision.spec.ts new file mode 100644 index 0000000..6513b1e --- /dev/null +++ b/scripts/vision.spec.ts @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + ExifMetadata, + ImageMetadataSuggestion, + VisionAIResult, +} from "./vision.ts"; +import { getImagesToProcess, mergeMetaAndVisionData } from "./vision.ts"; + +const FINAL: ImageMetadataSuggestion = { + id: "2R9A2805", + title: [ + "Blossom and Buzz", + "Spring's Gentle Awakening", + "Cherry Blossom Haven", + "Nature's Delicate Balance", + "A Bee's Spring Feast", + ], + image: "./2R9A2805.jpg", + alt: "Close-up of vibrant pink cherry blossoms on a branch with a honeybee collecting nectar. The bee's wings are slightly blurred, capturing its motion as it works. The background is a soft, dreamy pink hue, complementing the sharp details of the blossoms and the bee.", + location: "48 deg 8' 37.56\" N, 11 deg 34' 13.32\" E", + date: "2024-03-17", + tags: ["nature", "cherryblossom", "bee", "spring", "floral"], + exif: { + camera: "Canon EOS R6m2", + lens: "RF70-200mm F2.8 L IS USM", + aperture: "2.8", + iso: "125", + focal_length: "200.0", + shutter_speed: "1/1000", + }, +}; + +const VISION_DATA: VisionAIResult = { + title_ideas: [ + "Blossom and Buzz", + "Spring's Gentle Awakening", + "Cherry Blossom Haven", + "Nature's Delicate Balance", + "A Bee's Spring Feast", + ], + description: + "Close-up of vibrant pink cherry blossoms on a branch with a honeybee collecting nectar. The bee's wings are slightly blurred, capturing its motion as it works. The background is a soft, dreamy pink hue, complementing the sharp details of the blossoms and the bee.", + tags: ["nature", "cherryblossom", "bee", "spring", "floral"], +}; + +const EXIF_DATA: ExifMetadata = { + SourceFile: "/Users/flori/Sites/flori-dev/src/content/grid/2R9A2805.jpg", + FileName: "2R9A2805.jpg", + Model: "Canon EOS R6m2", + ExposureTime: "1/1000", + FNumber: 2.8, + ISO: 125, + DateTimeOriginal: "2024:03:17 15:06:16", + FocalLength: "200.0 mm", + LensModel: "RF70-200mm F2.8 L IS USM", + GPSPosition: "48 deg 8' 37.56\" N, 11 deg 34' 13.32\" E", +}; + +async function main() { + const tempRoot = await mkdtemp(join(tmpdir(), "vision-photos-")); + + try { + assert.deepEqual(mergeMetaAndVisionData(EXIF_DATA, VISION_DATA), FINAL); + + const albumDirectory = join(tempRoot, "chiang-mai"); + const missingImage = join(albumDirectory, "2025-10-06-121017.jpg"); + const completeImage = join(albumDirectory, "2025-10-06-121212.jpg"); + + await mkdir(albumDirectory, { recursive: true }); + await writeFile(missingImage, ""); + await writeFile(completeImage, ""); + await writeFile(join(albumDirectory, "2025-10-06-121212.json"), "{}"); + + assert.deepEqual(await getImagesToProcess(tempRoot), [missingImage]); + assert.deepEqual( + await getImagesToProcess(tempRoot, { refresh: true, exifOnly: false }), + [missingImage, completeImage], + ); + assert.deepEqual( + await getImagesToProcess(tempRoot, { refresh: false, exifOnly: true }), + [completeImage], + ); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } +} + +await main(); diff --git a/scripts/vision.ts b/scripts/vision.ts new file mode 100644 index 0000000..5db01b7 --- /dev/null +++ b/scripts/vision.ts @@ -0,0 +1,820 @@ +#!/usr/bin/env -S node --experimental-strip-types + +import { execFile } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import Anthropic from "@anthropic-ai/sdk"; +import { consola } from "consola"; +import OpenAI from "openai"; +import sharp from "sharp"; +import { + getImagesMissingMetadata, + getImagesWithExistingMetadata, + getMetadataPathForImage, + getPhotoAbsolutePath, + getPhotoDirectories, + PHOTOS_DIRECTORY, +} from "../src/lib/photo-albums.ts"; + +const execFileAsync = promisify(execFile); + +/** + * Define the directory where the images are located. + */ +const PHOTOS_DIR = PHOTOS_DIRECTORY; + +/** + * Instantiate the Anthropic client. + */ +type VisionProvider = "anthropic" | "openai"; + +let anthropic: Anthropic | undefined; +let openai: OpenAI | undefined; + +function getAnthropicClient(): Anthropic { + anthropic ??= new Anthropic({ maxRetries: 0 }); + return anthropic; +} + +function getOpenAIClient(): OpenAI { + openai ??= new OpenAI({ maxRetries: 0 }); + return openai; +} + +function assertRequiredEnvironment(provider: VisionProvider): void { + if (provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) { + throw new Error( + "Missing ANTHROPIC_API_KEY. `pnpm run vision` loads `.env.local` automatically. If you run the script directly, use `node --env-file=.env.local --experimental-strip-types scripts/vision.ts`.", + ); + } + if (provider === "openai" && !process.env.OPENAI_API_KEY) { + throw new Error("Missing OPENAI_API_KEY. Set it in `.env.local`."); + } +} + +/** + * Represents the metadata of an image in the Exif format. + */ +export interface ExifMetadata { + SourceFile: string; + FileName: string; + Model: string; + FNumber: number; + FocalLength: string; + ExposureTime: string; + ISO: number; + DateTimeOriginal: string; + LensModel: string; + GPSPosition?: string; + GPSLatitude?: string; + GPSLongitude?: string; + Keywords?: string | string[]; + Subject?: string | string[]; + Title?: string; + "Caption-Abstract"?: string; +} + +/** + * Represents the result of the AI analysis. + */ +export interface VisionAIResult { + title_ideas: string[]; + description: string; + tags: string[]; +} + +/** + * Represents the final metadata suggestion for an image. + */ +export interface ImageMetadataSuggestion { + id: string; + title: string[]; + image: string; + alt: string; + location: string; + locationName?: string; + date: string; + tags: string[]; + exif: { + camera: string; + lens: string; + aperture: string; + iso: string; + focal_length: string; + shutter_speed: string; + }; +} + +interface VisionCliOptions { + refresh: boolean; + exifOnly: boolean; + photosDirectory?: string; + visionConcurrency: number; + visionMaxRetries: number; + visionBaseBackoffMs: number; + provider: VisionProvider; +} + +function parseCliOptions(argv: string[]): VisionCliOptions { + const getNumericOption = (name: string, fallback: number): number => { + const prefix = `--${name}=`; + const rawValue = argv + .find((arg) => arg.startsWith(prefix)) + ?.slice(prefix.length); + const parsed = Number.parseInt(rawValue ?? "", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; + }; + + const envConcurrency = Number.parseInt( + process.env.VISION_CONCURRENCY ?? "", + 10, + ); + const envMaxRetries = Number.parseInt( + process.env.VISION_MAX_RETRIES ?? "", + 10, + ); + const envBaseBackoffMs = Number.parseInt( + process.env.VISION_BASE_BACKOFF_MS ?? "", + 10, + ); + const nonFlagArgs = argv.filter((arg) => !arg.startsWith("--")); + + const providerArg = argv + .find((arg) => arg.startsWith("--provider=")) + ?.slice("--provider=".length); + const envProvider = process.env.VISION_PROVIDER; + const rawProvider = providerArg ?? envProvider ?? "anthropic"; + const provider: VisionProvider = + rawProvider === "openai" ? "openai" : "anthropic"; + + return { + refresh: argv.includes("--refresh"), + exifOnly: argv.includes("--exif-only"), + provider, + photosDirectory: resolve(nonFlagArgs[0] ?? PHOTOS_DIR), + visionConcurrency: getNumericOption( + "concurrency", + Number.isFinite(envConcurrency) && envConcurrency > 0 + ? envConcurrency + : 2, + ), + visionMaxRetries: getNumericOption( + "retries", + Number.isFinite(envMaxRetries) && envMaxRetries > 0 ? envMaxRetries : 8, + ), + visionBaseBackoffMs: getNumericOption( + "backoff-ms", + Number.isFinite(envBaseBackoffMs) && envBaseBackoffMs > 0 + ? envBaseBackoffMs + : 1500, + ), + }; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function isRateLimitError(error: unknown): boolean { + return error instanceof Anthropic.RateLimitError; +} + +function extractRetryAfterMs(error: unknown): number | null { + if (!(error instanceof Anthropic.RateLimitError)) { + return null; + } + + const retryAfter = error.headers?.get("retry-after"); + if (retryAfter) { + const seconds = Number.parseFloat(retryAfter); + if (Number.isFinite(seconds) && seconds > 0) { + return Math.ceil(seconds * 1000); + } + } + + return null; +} + +async function mapWithConcurrency( + values: T[], + concurrency: number, + mapper: (value: T, index: number) => Promise, +): Promise { + if (values.length === 0) { + return []; + } + + const results: R[] = new Array(values.length); + const workerCount = Math.max(1, Math.min(concurrency, values.length)); + let cursor = 0; + + const workers = Array.from({ length: workerCount }, async () => { + while (true) { + const currentIndex = cursor; + cursor += 1; + + if (currentIndex >= values.length) { + return; + } + + const value = values[currentIndex]; + if (typeof value === "undefined") { + continue; + } + + results[currentIndex] = await mapper(value, currentIndex); + } + }); + + await Promise.all(workers); + return results; +} + +/** + * Get all images that don't have a JSON file and therefore need to be processed. + */ +export async function getImagesToProcess( + photosDirectory = PHOTOS_DIR, + options: Pick = { + refresh: false, + exifOnly: false, + }, +): Promise { + let relativeImagePaths: string[]; + let label: string; + + if (options.exifOnly) { + relativeImagePaths = await getImagesWithExistingMetadata(photosDirectory); + label = `Found ${relativeImagePaths.length} ${relativeImagePaths.length === 1 ? "image" : "images"} with existing metadata (EXIF-only update)`; + } else if (options.refresh) { + relativeImagePaths = (await getPhotoDirectories(photosDirectory)).flatMap( + (directory) => directory.imagePaths, + ); + label = `Refreshing ${relativeImagePaths.length} ${relativeImagePaths.length === 1 ? "image" : "images"} with metadata sidecars`; + } else { + relativeImagePaths = await getImagesMissingMetadata(photosDirectory); + label = `Found ${relativeImagePaths.length} ${relativeImagePaths.length === 1 ? "image" : "images"} without metadata`; + } + + consola.info(label); + + return relativeImagePaths.map((imagePath) => + getPhotoAbsolutePath(imagePath, photosDirectory), + ); +} + +/** + * Reads an existing JSON sidecar for an image, preserving all fields. + */ +async function readExistingJsonSidecar( + imagePath: string, + photosDirectory: string, +): Promise> { + const relativeImagePath = relative(photosDirectory, imagePath); + const jsonPath = getMetadataPathForImage(relativeImagePath, photosDirectory); + const content = await readFile(jsonPath, "utf-8"); + return JSON.parse(content) as Record; +} + +/** + * Updates only the EXIF-derived fields in an existing metadata object, + * preserving all other fields (title, alt, tags, flickrId, etc.). + */ +export function mergeExifIntoExisting( + exifData: ExifMetadata, + existing: Record, + locationName?: string | null, +): Record { + const [date] = exifData.DateTimeOriginal.split(" "); + + if (!date) { + throw new Error(`Missing original date for ${exifData.SourceFile}.`); + } + + const result: Record = { + ...existing, + location: getLocationFromExif(exifData), + date: date.replaceAll(":", "-"), + exif: { + camera: exifData.Model, + lens: exifData.LensModel, + aperture: exifData.FNumber.toString(), + iso: exifData.ISO.toString(), + focal_length: exifData.FocalLength.replace(" mm", ""), + shutter_speed: exifData.ExposureTime, + }, + }; + + if (locationName) { + result.locationName = locationName; + } + + return result; +} + +/** + * Extracts the EXIF metadata from an image file. + * @param imagePath - The path to the image file. + * + * @returns A promise that resolves to the extracted EXIF metadata. + */ +export async function extractExifMetadata( + imagePath: string, +): Promise { + /// Check if `exiftool` is installed. + try { + await execFileAsync("exiftool", ["--version"]); + } catch (_error) { + consola.error( + "exiftool is not installed. Please run `brew install exiftool`.", + ); + process.exit(1); + } + + /// Extract the metadata + const { stdout } = await execFileAsync("exiftool", ["-j", imagePath]); + const output = JSON.parse(stdout) as ExifMetadata[]; + + if (!output[0]) { + throw new Error(`No EXIF metadata found for ${imagePath}.`); + } + + return output[0]; +} + +/** + * Encodes an image file to base64. + * @param imagePath - The path to the image file. + * @returns A Promise that resolves to the base64 encoded image. + */ +/** + * The Vision API internally downscales to max 1568px on the longest side. + * Anything larger wastes tokens without improving results. + */ +const VISION_MAX_DIMENSION = 1568; + +async function base64EncodeImage(imagePath: string): Promise { + const resized = await sharp(imagePath) + .resize({ + width: VISION_MAX_DIMENSION, + height: VISION_MAX_DIMENSION, + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ quality: 80 }) + .toBuffer(); + + return resized.toString("base64"); +} + +const VISION_TOOL = { + name: "vision_response", + description: "Return the vision analysis of the image.", + input_schema: { + type: "object" as const, + additionalProperties: false, + properties: { + title_ideas: { type: "array", items: { type: "string" } }, + description: { type: "string" }, + tags: { type: "array", items: { type: "string" } }, + }, + required: ["title_ideas", "description", "tags"], + }, +}; + +/** + * Generates image description, title suggestions and tags using AI. + * + * @param metadata - The metadata of the image. + * @returns A Promise that resolves to a VisionAIResult object containing the generated image description, title suggestions, and tags. + */ +function buildVisionPrompt( + metadata: ExifMetadata, + locationName: string | null, +): string { + const locationContext = locationName + ? ` Das Foto wurde aufgenommen in: ${locationName}. Verwende diesen Ort konkret in der Beschreibung.` + : ""; + + const rawKeywords = metadata.Keywords ?? metadata.Subject ?? []; + const keywords = Array.isArray(rawKeywords) ? rawKeywords : [rawKeywords]; + const keywordContext = + keywords.length > 0 + ? ` Folgende Tags sind vom Fotografen vergeben worden: ${keywords.join(", ")}. Beruecksichtige diese Informationen in der Beschreibung.` + : ""; + + return `Erstelle eine präzise und detaillierte Beschreibung dieses Bildes, die auch als Alt-Text funktioniert. Der Alt-Text soll keine Wörter wie Bild, Foto, Fotografie, Illustration oder Ähnliches enthalten. Beschreibe die Szene so, wie sie ist.${locationContext}${keywordContext} Erstelle außerdem 5 Titelvorschläge für dieses Bild. Schlage zuletzt 5 Tags vor, die zur Bildbeschreibung passen. Diese Tags sollen einzelne Wörter sein. Identifiziere das Hauptmotiv oder Thema und stelle den entsprechenden Tag an die erste Stelle. Gib die Beschreibung, die Titelvorschläge und die Tags zurück.`; +} + +async function callAnthropicVision( + encodedImage: string, + prompt: string, + sourceFile: string, +): Promise { + const response = await getAnthropicClient().messages.create({ + model: "claude-opus-4-6", + max_tokens: 2048, + tools: [VISION_TOOL], + tool_choice: { type: "tool", name: "vision_response" }, + messages: [ + { + role: "user", + content: [ + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: encodedImage, + }, + }, + { type: "text", text: prompt }, + ], + }, + ], + }); + + const toolUseBlock = response.content.find((b) => b.type === "tool_use"); + if (!toolUseBlock || toolUseBlock.type !== "tool_use") { + throw new Error(`No tool use response from AI for ${sourceFile}.`); + } + + return toolUseBlock.input as VisionAIResult; +} + +async function callOpenAIVision( + encodedImage: string, + prompt: string, + sourceFile: string, +): Promise { + const jsonPrompt = `${prompt}\n\nWICHTIG: Antworte komplett auf Deutsch. Alle Titel, Beschreibungen und Tags muessen auf Deutsch sein.\n\nAntworte ausschliesslich mit einem JSON-Objekt im folgenden Format:\n{"title_ideas": ["...", "...", "...", "...", "..."], "description": "...", "tags": ["...", "...", "...", "...", "..."]}`; + + const response = await getOpenAIClient().chat.completions.create({ + model: "gpt-4o-mini", + max_tokens: 2048, + response_format: { type: "json_object" }, + messages: [ + { + role: "user", + content: [ + { + type: "image_url", + image_url: { url: `data:image/jpeg;base64,${encodedImage}` }, + }, + { type: "text", text: jsonPrompt }, + ], + }, + ], + }); + + const content = response.choices[0]?.message?.content; + if (!content) { + throw new Error(`No response from OpenAI for ${sourceFile}.`); + } + + return JSON.parse(content) as VisionAIResult; +} + +function isRetryableError(error: unknown, provider: VisionProvider): boolean { + if (provider === "anthropic") { + return isRateLimitError(error); + } + if (error instanceof OpenAI.RateLimitError) { + return true; + } + return false; +} + +async function generateImageDescriptionTitleSuggestionsAndTags( + metadata: ExifMetadata, + locationName: string | null, + options: Pick< + VisionCliOptions, + "visionMaxRetries" | "visionBaseBackoffMs" | "provider" + >, +): Promise { + const encodedImage = await base64EncodeImage(metadata.SourceFile); + const prompt = buildVisionPrompt(metadata, locationName); + + let lastError: unknown; + + for (let attempt = 0; attempt <= options.visionMaxRetries; attempt += 1) { + try { + const result = + options.provider === "openai" + ? await callOpenAIVision(encodedImage, prompt, metadata.SourceFile) + : await callAnthropicVision( + encodedImage, + prompt, + metadata.SourceFile, + ); + + if ( + result.title_ideas.length === 0 || + result.description.length === 0 || + result.tags.length === 0 + ) { + throw new Error( + `Incomplete vision response for ${metadata.SourceFile}.`, + ); + } + + return result; + } catch (error) { + lastError = error; + if ( + !isRetryableError(error, options.provider) || + attempt >= options.visionMaxRetries + ) { + break; + } + + const retryAfterMs = extractRetryAfterMs(error); + const exponentialBackoffMs = options.visionBaseBackoffMs * 2 ** attempt; + const jitterMs = Math.floor(Math.random() * 350); + const waitMs = + Math.max(retryAfterMs ?? 0, exponentialBackoffMs) + jitterMs; + const relativeSourcePath = relative(process.cwd(), metadata.SourceFile); + const nextAttempt = attempt + 1; + consola.warn( + `Rate limit for ${relativeSourcePath}. Retry ${nextAttempt}/${options.visionMaxRetries} in ${Math.ceil(waitMs / 1000)}s...`, + ); + await sleep(waitMs); + } + } + + throw lastError; +} + +function ensureVisionCanRun( + imagesToProcess: string[], + provider: VisionProvider, +): void { + if (imagesToProcess.length === 0) { + return; + } + + assertRequiredEnvironment(provider); +} + +/** + * Parses an EXIF DMS string like `7 deg 49' 12.00" N` into a decimal number. + */ +function parseDms(dms: string): number { + const match = dms.match(/(\d+)\s*deg\s*(\d+)'\s*([\d.]+)"\s*([NSEW])/i); + if (!match) { + return Number.NaN; + } + const [, deg, min, sec, dir] = match; + let decimal = Number(deg) + Number(min) / 60 + Number(sec) / 3600; + if (dir === "S" || dir === "W") { + decimal *= -1; + } + return decimal; +} + +/** + * Resolves GPS coordinates to a human-readable location via Nominatim. + */ +async function reverseGeocode( + lat: number, + lon: number, +): Promise { + const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=de&zoom=14`; + try { + const response = await fetch(url, { + headers: { "User-Agent": "adrian-altner.de/vision-script" }, + }); + if (!response.ok) return null; + const data = (await response.json()) as { display_name?: string }; + return data.display_name ?? null; + } catch { + return null; + } +} + +/** + * Resolves EXIF GPS data to a readable location name. Returns null if no GPS data. + */ +async function resolveLocationName( + exifData: ExifMetadata, +): Promise { + const latStr = exifData.GPSLatitude; + const lonStr = exifData.GPSLongitude; + if (!latStr || !lonStr) return null; + + const lat = parseDms(latStr); + const lon = parseDms(lonStr); + if (Number.isNaN(lat) || Number.isNaN(lon)) return null; + + return await reverseGeocode(lat, lon); +} + +function getLocationFromExif(exifData: ExifMetadata): string { + if (exifData.GPSPosition) { + return exifData.GPSPosition; + } + + if (exifData.GPSLatitude && exifData.GPSLongitude) { + return `${exifData.GPSLatitude}, ${exifData.GPSLongitude}`; + } + + return ""; +} + +/** + * Merges the metadata from EXIF data and vision data to create an ImageMetadataSuggestion object. + * @param exifData - The EXIF metadata of the image. + * @param visionData - The vision AI result data of the image. + * @returns The merged ImageMetadataSuggestion object. + */ +export function mergeMetaAndVisionData( + exifData: ExifMetadata, + visionData: VisionAIResult, + locationName?: string | null, +): ImageMetadataSuggestion { + const [date] = exifData.DateTimeOriginal.split(" "); + + if (!date) { + throw new Error(`Missing original date for ${exifData.SourceFile}.`); + } + + const result: ImageMetadataSuggestion = { + id: exifData.FileName.replace(".jpg", ""), + title: visionData.title_ideas, + image: `./${exifData.FileName}`, + alt: visionData.description, + location: getLocationFromExif(exifData), + date: date.replaceAll(":", "-"), + tags: visionData.tags, + exif: { + camera: exifData.Model, + lens: exifData.LensModel, + aperture: exifData.FNumber.toString(), + iso: exifData.ISO.toString(), + focal_length: exifData.FocalLength.replace(" mm", ""), + shutter_speed: exifData.ExposureTime, + }, + }; + + if (locationName) { + result.locationName = locationName; + } + + return result; +} + +/** + * Writes the given image metadata to a JSON file. + * @param imageMetadata - The image metadata to be written. + * @returns A Promise that resolves when the JSON file is written successfully. + */ +async function writeToJsonFile( + imageMetadata: ImageMetadataSuggestion, + imagePath: string, + photosDirectory: string, +): Promise { + const relativeImagePath = relative(photosDirectory, imagePath); + const jsonPath = getMetadataPathForImage(relativeImagePath, photosDirectory); + const json = JSON.stringify(imageMetadata, null, 2); + await writeFile(jsonPath, json); +} + +/** + * Main. + */ +async function main() { + consola.start("Checking for images to process..."); + const cliOptions = parseCliOptions(process.argv.slice(2)); + const photosDirectory = cliOptions.photosDirectory ?? PHOTOS_DIR; + + /// Load all images that don't have a JSON file. + const images = await getImagesToProcess(photosDirectory, cliOptions); + + if (images.length === 0) { + consola.success( + cliOptions.exifOnly + ? "No images with existing metadata found." + : cliOptions.refresh + ? "No images found to refresh." + : "No images require metadata.", + ); + return; + } + + /// Extract EXIF metadata from these images. + const exifData = await mapWithConcurrency( + images, + 8, + async (imagePath, index) => { + consola.info(`Extracting EXIF ${index + 1}/${images.length}...`); + return await extractExifMetadata(imagePath); + }, + ); + + /// Resolve location names via Nominatim (sequential, 1 req/s limit). + consola.info("Resolving location names via Nominatim..."); + const locationNames: (string | null)[] = []; + for (const exifEntry of exifData) { + const name = await resolveLocationName(exifEntry); + locationNames.push(name); + if (name) { + await sleep(1000); + } + } + const resolvedCount = locationNames.filter(Boolean).length; + consola.info(`Resolved ${resolvedCount}/${exifData.length} location names.`); + + if (cliOptions.exifOnly) { + /// EXIF-only mode: read existing JSON, merge EXIF fields, write back. + const existingData = await mapWithConcurrency( + images, + 8, + async (imagePath) => readExistingJsonSidecar(imagePath, photosDirectory), + ); + + await mapWithConcurrency(exifData, 8, async (exifEntry, index) => { + const existing = existingData[index]; + + if (!existing) { + throw new Error( + `Missing existing metadata for ${exifEntry.SourceFile}.`, + ); + } + + const updated = mergeExifIntoExisting( + exifEntry, + existing, + locationNames[index], + ); + const relativeImagePath = relative(photosDirectory, exifEntry.SourceFile); + const jsonPath = getMetadataPathForImage( + relativeImagePath, + photosDirectory, + ); + await writeFile(jsonPath, JSON.stringify(updated, null, 2)); + consola.info( + `Updated EXIF ${index + 1}/${exifData.length}: ${relativeImagePath}`, + ); + }); + + consola.success("All EXIF data updated successfully."); + return; + } + + consola.info( + `Vision settings: provider=${cliOptions.provider}, concurrency=${cliOptions.visionConcurrency}, retries=${cliOptions.visionMaxRetries}, backoff=${cliOptions.visionBaseBackoffMs}ms`, + ); + + ensureVisionCanRun(images, cliOptions.provider); + + /// Determine the image description, title suggestions and tags for each image with AI. + const visionData = await mapWithConcurrency( + exifData, + cliOptions.visionConcurrency, + async (exifEntry, index) => { + consola.info(`Generating AI metadata ${index + 1}/${exifData.length}...`); + return await generateImageDescriptionTitleSuggestionsAndTags( + exifEntry, + locationNames[index] ?? null, + cliOptions, + ); + }, + ); + + /// Merge the EXIF and Vision data to create the final metadata suggestion. + const imageData = exifData.map((e, i) => { + const currentVisionData = visionData[i]; + + if (!currentVisionData) { + throw new Error(`Missing vision data for ${e.SourceFile}.`); + } + + return mergeMetaAndVisionData(e, currentVisionData, locationNames[i]); + }); + + /// Write the metadata to JSON files. + await mapWithConcurrency(imageData, 8, async (imageMetadata, index) => { + const sourceFile = exifData[index]?.SourceFile; + + if (!sourceFile) { + throw new Error(`Missing source file for ${imageMetadata.id}.`); + } + + await writeToJsonFile(imageMetadata, sourceFile, photosDirectory); + consola.info(`Wrote metadata ${index + 1}/${imageData.length}.`); + }); + + consola.success("All images processed successfully."); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + try { + await main(); + } catch (error) { + consola.error(error); + process.exit(1); + } +} diff --git a/src/assets/blog-placeholder-1.jpg b/src/assets/blog-placeholder-1.jpg new file mode 100644 index 0000000..74d4009 Binary files /dev/null and b/src/assets/blog-placeholder-1.jpg differ diff --git a/src/assets/blog-placeholder-2.jpg b/src/assets/blog-placeholder-2.jpg new file mode 100644 index 0000000..c4214b0 Binary files /dev/null and b/src/assets/blog-placeholder-2.jpg differ diff --git a/src/assets/blog-placeholder-3.jpg b/src/assets/blog-placeholder-3.jpg new file mode 100644 index 0000000..fbe2ac0 Binary files /dev/null and b/src/assets/blog-placeholder-3.jpg differ diff --git a/src/assets/blog-placeholder-4.jpg b/src/assets/blog-placeholder-4.jpg new file mode 100644 index 0000000..f4fc88e Binary files /dev/null and b/src/assets/blog-placeholder-4.jpg differ diff --git a/src/assets/blog-placeholder-5.jpg b/src/assets/blog-placeholder-5.jpg new file mode 100644 index 0000000..c564674 Binary files /dev/null and b/src/assets/blog-placeholder-5.jpg differ diff --git a/src/assets/blog-placeholder-about.jpg b/src/assets/blog-placeholder-about.jpg new file mode 100644 index 0000000..cf5f685 Binary files /dev/null and b/src/assets/blog-placeholder-about.jpg differ diff --git a/src/assets/fonts/atkinson-bold.woff b/src/assets/fonts/atkinson-bold.woff new file mode 100644 index 0000000..e7f8977 Binary files /dev/null and b/src/assets/fonts/atkinson-bold.woff differ diff --git a/src/assets/fonts/atkinson-regular.woff b/src/assets/fonts/atkinson-regular.woff new file mode 100644 index 0000000..bbe09c5 Binary files /dev/null and b/src/assets/fonts/atkinson-regular.woff differ diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro new file mode 100644 index 0000000..07a4e18 --- /dev/null +++ b/src/components/BaseHead.astro @@ -0,0 +1,70 @@ +--- +// Import the global.css file here so that it is included on +// all pages through the use of the component. +import '~/styles/global.css'; +import type { ImageMetadata } from 'astro'; +import FallbackImage from '~/assets/blog-placeholder-1.jpg'; +import { DEFAULT_LOCALE, type Locale, SITE } from '~/consts'; +import { getLocaleFromUrl, switchLocalePath } from '~/i18n/ui'; +import { Font } from 'astro:assets'; + +interface Props { + title: string; + description: string; + image?: ImageMetadata; + locale?: Locale; +} + +const canonicalURL = new URL(Astro.url.pathname, Astro.site); +const { + title, + description, + image = FallbackImage, + locale = getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE, +} = Astro.props; + +const otherLocale: Locale = locale === 'de' ? 'en' : 'de'; +const alternateHref = new URL(switchLocalePath(Astro.url.pathname, otherLocale), Astro.site); +const rssHref = new URL(locale === 'de' ? 'rss.xml' : 'en/rss.xml', Astro.site); +--- + + + + + + + + + + + + + + + + + + + + + + + +{title} + + + + + + + + + + + + + + + + + diff --git a/src/components/CategoriesPage.astro b/src/components/CategoriesPage.astro new file mode 100644 index 0000000..31eeb35 --- /dev/null +++ b/src/components/CategoriesPage.astro @@ -0,0 +1,62 @@ +--- +import BaseLayout from '~/layouts/BaseLayout.astro'; +import { type Locale, SITE } from '~/consts'; +import { categoryHref, getCategoriesByLocale, getPostsByCategory } from '~/i18n/posts'; +import { t } from '~/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const categories = await getCategoriesByLocale(locale); +const withCounts = await Promise.all( + categories.map(async (c) => ({ category: c, count: (await getPostsByCategory(c)).length })), +); + +const pageTitle = `${t(locale, 'categories.title')} — ${SITE[locale].title}`; +const pageDescription = t(locale, 'categories.description'); +--- + + +
+

{t(locale, 'categories.title')}

+

{pageDescription}

+
    + { + withCounts.map(({ category, count }) => ( +
  • + + {category.data.name} + + ({count}) + {category.data.description &&

    {category.data.description}

    } +
  • + )) + } +
+
+
+ + diff --git a/src/components/CategoryDetailPage.astro b/src/components/CategoryDetailPage.astro new file mode 100644 index 0000000..bd8ccf9 --- /dev/null +++ b/src/components/CategoryDetailPage.astro @@ -0,0 +1,88 @@ +--- +import { Image } from 'astro:assets'; +import type { CollectionEntry } from 'astro:content'; +import FormattedDate from '~/components/FormattedDate.astro'; +import BaseLayout from '~/layouts/BaseLayout.astro'; +import { type Locale, SITE } from '~/consts'; +import { getPostsByCategory, postSlug } from '~/i18n/posts'; +import { localizePath, t } from '~/i18n/ui'; + +interface Props { + locale: Locale; + category: CollectionEntry<'categories'>; +} + +const { locale, category } = Astro.props; +const posts = await getPostsByCategory(category); +const pageTitle = `${category.data.name} — ${SITE[locale].title}`; +const pageDescription = + category.data.description ?? `${t(locale, 'category.postsIn')} ${category.data.name}`; +--- + + +
+

{category.data.name}

+ {category.data.description &&

{category.data.description}

} +

{t(locale, 'category.postsIn')} {category.data.name}

+ { + posts.length === 0 ? ( +

{t(locale, 'category.noPosts')}

+ ) : ( + + ) + } +
+
+ + diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..ec7a5f0 --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,91 @@ +--- +import { DEFAULT_LOCALE, type Locale } from '~/consts'; +import { getLocaleFromUrl, localizePath, t } from '~/i18n/ui'; + +interface Props { + locale?: Locale; +} + +const locale: Locale = Astro.props.locale ?? getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE; +const contactSegment = locale === 'de' ? 'kontakt' : 'contact'; +const imprintSegment = locale === 'de' ? 'impressum' : 'imprint'; +const privacySegment = locale === 'de' ? 'datenschutz' : 'privacy-policy'; + +const today = new Date(); +--- + + + diff --git a/src/components/FormattedDate.astro b/src/components/FormattedDate.astro new file mode 100644 index 0000000..db993cf --- /dev/null +++ b/src/components/FormattedDate.astro @@ -0,0 +1,27 @@ +--- +import { DEFAULT_LOCALE, type Locale } from '~/consts'; +import { getLocaleFromUrl } from '~/i18n/ui'; + +interface Props { + date: Date; + locale?: Locale; + class?: string; +} + +const { + date, + locale = getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE, + class: className, +} = Astro.props; +const tag = locale === 'de' ? 'de-DE' : 'en-US'; +--- + + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 0000000..8e28250 --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,433 @@ +--- +import type { CollectionEntry } from 'astro:content'; +import { type Locale, SITE } from '~/consts'; +import { + aboutSegment, + categoryIndexSegment, + entryHref, + findTagBySlug, + findTranslation, + tagHref, + tagIndexSegment, + tagSegment, +} from '~/i18n/posts'; +import { getLocaleFromUrl, localizePath, switchLocalePath, t } from '~/i18n/ui'; +import HeaderLink from '~/components/HeaderLink.astro'; + +interface Props { + locale?: Locale; + /** The current page's content entry, if any — used to resolve a translated language-switch link. */ + entry?: CollectionEntry<'posts' | 'categories'>; +} + +const { entry } = Astro.props; +const locale: Locale = Astro.props.locale ?? getLocaleFromUrl(Astro.url); +const otherLocale: Locale = locale === 'de' ? 'en' : 'de'; +const homeHref = localizePath('/', locale); +const aboutHref = localizePath(`/${aboutSegment(locale)}`, locale); +const categoriesHref = localizePath(`/${categoryIndexSegment(locale)}`, locale); +const tagsHref = localizePath(`/${tagIndexSegment(locale)}`, locale); + +const translated = entry ? await findTranslation(entry, otherLocale) : undefined; + +// If we're on a tag detail page, verify the tag exists in the target locale — +// otherwise the switched URL would 404. Fall back to the target tags index. +async function resolveSwitchHref(): Promise { + if (translated) return entryHref(translated); + if (entry) return localizePath('/', otherLocale); + const parts = Astro.url.pathname.split('/').filter(Boolean); + const localePrefix = parts[0] === locale ? 1 : 0; + const first = parts[localePrefix]; + const slug = parts[localePrefix + 1]; + if (first === tagSegment(locale) && slug) { + const target = await findTagBySlug(otherLocale, slug); + return target + ? tagHref(otherLocale, target) + : localizePath(`/${tagIndexSegment(otherLocale)}`, otherLocale); + } + return switchLocalePath(Astro.url.pathname, otherLocale); +} +const switchHref = await resolveSwitchHref(); +--- + +
+ +
+ + + diff --git a/src/components/HeaderLink.astro b/src/components/HeaderLink.astro new file mode 100644 index 0000000..77cf777 --- /dev/null +++ b/src/components/HeaderLink.astro @@ -0,0 +1,42 @@ +--- +import type { HTMLAttributes } from 'astro/types'; +import { LOCALES } from '~/consts'; + +type Props = HTMLAttributes<'a'>; + +const { href, class: className, ...props } = Astro.props; + +function stripTrailing(p: string) { + return p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p; +} + +function stripBase(p: string) { + const base = import.meta.env.BASE_URL; + if (base && base !== '/' && p.startsWith(base)) return p.slice(base.length - 1) || '/'; + return p; +} + +const pathname = stripTrailing(stripBase(Astro.url.pathname)); +const target = stripTrailing(String(href ?? '')); + +// Locale home URLs (`/`, `/en`) should only activate on exact match; deeper +// routes activate when the pathname equals or is nested under the href. +const localeHomes = new Set(['/', ...LOCALES.map((l) => `/${l}`)]); +const isActive = localeHomes.has(target) + ? pathname === target + : pathname === target || pathname.startsWith(target + '/'); +--- + + + + + diff --git a/src/components/HomePage.astro b/src/components/HomePage.astro new file mode 100644 index 0000000..6cb3e9e --- /dev/null +++ b/src/components/HomePage.astro @@ -0,0 +1,118 @@ +--- +import { Image } from 'astro:assets'; +import FormattedDate from '~/components/FormattedDate.astro'; +import BaseLayout from '~/layouts/BaseLayout.astro'; +import { type Locale, SITE } from '~/consts'; +import { getPostsByLocale, postSlug } from '~/i18n/posts'; +import { localizePath } from '~/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const posts = await getPostsByLocale(locale); +--- + + +
+ +
+
+ + diff --git a/src/components/TagDetailPage.astro b/src/components/TagDetailPage.astro new file mode 100644 index 0000000..810bb0f --- /dev/null +++ b/src/components/TagDetailPage.astro @@ -0,0 +1,84 @@ +--- +import { Image } from 'astro:assets'; +import FormattedDate from '~/components/FormattedDate.astro'; +import BaseLayout from '~/layouts/BaseLayout.astro'; +import { type Locale, SITE } from '~/consts'; +import { type TagEntry, getPostsByTag, postSlug } from '~/i18n/posts'; +import { localizePath, t } from '~/i18n/ui'; + +interface Props { + locale: Locale; + tag: TagEntry; +} + +const { locale, tag } = Astro.props; +const posts = await getPostsByTag(locale, tag.slug); +const pageTitle = `${tag.name} — ${SITE[locale].title}`; +const pageDescription = `${t(locale, 'tag.postsTagged')} ${tag.name}`; +--- + + + + + + diff --git a/src/components/TagsPage.astro b/src/components/TagsPage.astro new file mode 100644 index 0000000..1beceb3 --- /dev/null +++ b/src/components/TagsPage.astro @@ -0,0 +1,57 @@ +--- +import BaseLayout from '~/layouts/BaseLayout.astro'; +import { type Locale, SITE } from '~/consts'; +import { getTagsByLocale, tagHref } from '~/i18n/posts'; +import { t } from '~/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const tags = await getTagsByLocale(locale); +const pageTitle = `${t(locale, 'tags.title')} — ${SITE[locale].title}`; +const pageDescription = t(locale, 'tags.description'); +--- + + +
+

{t(locale, 'tags.title')}

+

{pageDescription}

+ +
+
+ + diff --git a/src/components/Webmentions.astro b/src/components/Webmentions.astro new file mode 100644 index 0000000..5d0f588 --- /dev/null +++ b/src/components/Webmentions.astro @@ -0,0 +1,300 @@ +--- +import { DEFAULT_LOCALE, type Locale } from '~/consts'; +import { getLocaleFromUrl, t } from '~/i18n/ui'; + +declare global { + // eslint-disable-next-line no-var + var __WEBMENTION_TOKEN__: string; +} +const tokenRaw = (globalThis as unknown as { __WEBMENTION_TOKEN__?: string }).__WEBMENTION_TOKEN__; +const WEBMENTION_TOKEN = typeof tokenRaw === 'string' ? tokenRaw : ''; + +interface WMAuthor { + name?: string; + url?: string; + photo?: string; +} +interface WMEntry { + author?: WMAuthor; + url: string; + published?: string; + 'wm-received'?: string; + 'wm-id'?: number; + 'wm-property'?: string; + content?: { text?: string }; +} + +interface Props { + target: string | URL; + locale?: Locale; +} + +const { target, locale = getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE } = Astro.props; + +async function fetchMentions(target: string): Promise { + const token = WEBMENTION_TOKEN; + if (!token) return []; + const withSlash = target.endsWith('/') ? target : `${target}/`; + const withoutSlash = target.replace(/\/+$/, ''); + const fetchOne = async (t: string) => { + const url = new URL('https://webmention.io/api/mentions.jf2'); + url.searchParams.set('target', t); + url.searchParams.set('token', token); + url.searchParams.set('per-page', '100'); + const res = await fetch(url); + if (!res.ok) return [] as WMEntry[]; + const json = (await res.json()) as { children?: WMEntry[] }; + return json.children ?? []; + }; + const [a, b] = await Promise.all([fetchOne(withSlash), fetchOne(withoutSlash)]); + const seen = new Set(); + const merged: WMEntry[] = []; + for (const m of [...a, ...b]) { + const id = m['wm-id']; + if (id == null || seen.has(id)) continue; + seen.add(id); + merged.push(m); + } + return merged; +} + +const targetStr = target.toString(); +const all = await fetchMentions(targetStr); + +const likes = all.filter((m) => m['wm-property'] === 'like-of'); +const reposts = all.filter((m) => m['wm-property'] === 'repost-of'); +const replies = all.filter((m) => m['wm-property'] === 'in-reply-to'); +const mentions = all.filter( + (m) => !['like-of', 'repost-of', 'in-reply-to', 'bookmark-of'].includes(m['wm-property'] ?? ''), +); +const facepile = [...likes, ...reposts]; + +function authorInitial(m: WMEntry) { + return m.author?.name?.trim()?.[0]?.toUpperCase() ?? '?'; +} + +function formatDate(iso?: string) { + if (!iso) return ''; + const d = new Date(iso); + return d.toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +const hasAny = facepile.length > 0 || replies.length > 0 || mentions.length > 0; +--- + +{ + hasAny && ( +
+

{t(locale, 'webmentions.title')}

+ + {facepile.length > 0 && ( +
+ {likes.length > 0 && ( +
+

+ {`${likes.length} ${t(locale, likes.length === 1 ? 'webmentions.like' : 'webmentions.likes')}`} +

+ +
+ )} + + {reposts.length > 0 && ( +
+

+ {`${reposts.length} ${t(locale, reposts.length === 1 ? 'webmentions.repost' : 'webmentions.reposts')}`} +

+ +
+ )} +
+ )} + + {replies.length > 0 && ( +
+

{t(locale, 'webmentions.replies')}

+
    + {replies.map((m) => ( +
  1. + + {m.content?.text &&

    {m.content.text}

    } +
  2. + ))} +
+
+ )} + + {mentions.length > 0 && ( +
+

{t(locale, 'webmentions.mentions')}

+ +
+ )} +
+ ) +} + + diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..828f139 --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,14 @@ +export const LOCALES = ['de', 'en'] as const; +export type Locale = (typeof LOCALES)[number]; +export const DEFAULT_LOCALE: Locale = 'de'; + +export const SITE: Record = { + de: { + title: 'Adrian Altner', + description: 'Willkommen auf meiner Website!', + }, + en: { + title: 'Adrian Altner', + description: 'Welcome to my website!', + }, +}; diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 0000000..b7f64de --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,42 @@ +import { defineCollection, reference } from 'astro:content'; +import { glob } from 'astro/loaders'; +import { z } from 'astro/zod'; + +// Shared per-locale layout: +// src/content/posts//… — posts +// src/content/categories//… — categories +// Entry `id` is always `/`. A blog post's `category` references a +// category by that id (e.g. "de/technik" or "en/tech"). + +// Optional `translationKey`: entries in different locales that represent the +// same logical piece of content share one key. Used to wire up the language +// switcher so it points at the translated URL instead of 404-ing. + +const posts = defineCollection({ + loader: glob({ base: './src/content/posts', pattern: '{de,en}/**/*.{md,mdx}' }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + heroImage: z.optional(image()), + category: z.optional(reference('categories')), + // Free-form tags (aka Stichwörter). Plain strings kept inline on each + // post; no separate collection. The tag listing pages aggregate them + // across posts per locale. + tags: z.array(z.string()).optional(), + translationKey: z.string().optional(), + }), +}); + +const categories = defineCollection({ + loader: glob({ base: './src/content/categories', pattern: '{de,en}/**/*.md' }), + schema: z.object({ + name: z.string(), + description: z.string().optional(), + translationKey: z.string().optional(), + }), +}); + +export const collections = { posts, categories }; diff --git a/src/content/categories/de/allgemein.md b/src/content/categories/de/allgemein.md new file mode 100644 index 0000000..18f362c --- /dev/null +++ b/src/content/categories/de/allgemein.md @@ -0,0 +1,5 @@ +--- +name: Allgemein +description: Sammelkategorie für alles andere. +translationKey: general +--- diff --git a/src/content/categories/de/technik.md b/src/content/categories/de/technik.md new file mode 100644 index 0000000..3d45c4f --- /dev/null +++ b/src/content/categories/de/technik.md @@ -0,0 +1,5 @@ +--- +name: Technik +description: Beiträge rund um Technik, Entwicklung und Tools. +translationKey: tech +--- diff --git a/src/content/categories/en/general.md b/src/content/categories/en/general.md new file mode 100644 index 0000000..b74e487 --- /dev/null +++ b/src/content/categories/en/general.md @@ -0,0 +1,5 @@ +--- +name: General +description: Catch-all category. +translationKey: general +--- diff --git a/src/content/categories/en/tech.md b/src/content/categories/en/tech.md new file mode 100644 index 0000000..058a99f --- /dev/null +++ b/src/content/categories/en/tech.md @@ -0,0 +1,5 @@ +--- +name: Tech +description: Posts about technology, development and tools. +translationKey: tech +--- diff --git a/src/content/posts/de/first-post.md b/src/content/posts/de/first-post.md new file mode 100644 index 0000000..80f7b4e --- /dev/null +++ b/src/content/posts/de/first-post.md @@ -0,0 +1,18 @@ +--- +title: 'First post' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 08 2022' +heroImage: '../../../assets/blog-placeholder-3.jpg' +category: de/allgemein +translationKey: hello-world +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/src/content/posts/de/markdown-style-guide.md b/src/content/posts/de/markdown-style-guide.md new file mode 100644 index 0000000..c8e002f --- /dev/null +++ b/src/content/posts/de/markdown-style-guide.md @@ -0,0 +1,218 @@ +--- +title: 'Markdown Style Guide' +description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.' +pubDate: 'Jun 19 2024' +heroImage: '../../../assets/blog-placeholder-1.jpg' +category: de/technik +tags: + - markdown + - astro +--- + +Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro. + +## Headings + +The following HTML `

`—`

` elements represent six levels of section headings. `

` is the highest section level while `

` is the lowest. + +# H1 + +## H2 + +### H3 + +#### H4 + +##### H5 + +###### H6 + +## Paragraph + +Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat. + +Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat. + +## Images + +### Syntax + +```markdown +![Alt text](./full/or/relative/path/of/image) +``` + +### Output + +![blog placeholder](../../../assets/blog-placeholder-about.jpg) + +## Blockquotes + +The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations. + +### Blockquote without attribution + +#### Syntax + +```markdown +> Tiam, ad mint andaepu dandae nostion secatur sequo quae. +> **Note** that you can use _Markdown syntax_ within a blockquote. +``` + +#### Output + +> Tiam, ad mint andaepu dandae nostion secatur sequo quae. +> **Note** that you can use _Markdown syntax_ within a blockquote. + +### Blockquote with attribution + +#### Syntax + +```markdown +> Don't communicate by sharing memory, share memory by communicating.
+> — Rob Pike[^1] +``` + +#### Output + +> Don't communicate by sharing memory, share memory by communicating.
+> — Rob Pike[^1] + +[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015. + +## Tables + +### Syntax + +```markdown +| Italics | Bold | Code | +| --------- | -------- | ------ | +| _italics_ | **bold** | `code` | +``` + +### Output + +| Italics | Bold | Code | +| --------- | -------- | ------ | +| _italics_ | **bold** | `code` | + +## Code Blocks + +### Syntax + +we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntax, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash + +````markdown +```html + + + + + Example HTML5 Document + + +

Test

+ + +``` +```` + +### Output + +```html + + + + + Example HTML5 Document + + +

Test

+ + +``` + +## List Types + +### Ordered List + +#### Syntax + +```markdown +1. First item +2. Second item +3. Third item +``` + +#### Output + +1. First item +2. Second item +3. Third item + +### Unordered List + +#### Syntax + +```markdown +- List item +- Another item +- And another item +``` + +#### Output + +- List item +- Another item +- And another item + +### Nested list + +#### Syntax + +```markdown +- Fruit + - Apple + - Orange + - Banana +- Dairy + - Milk + - Cheese +``` + +#### Output + +- Fruit + - Apple + - Orange + - Banana +- Dairy + - Milk + - Cheese + +## Other Elements — abbr, sub, sup, kbd, mark + +### Syntax + +```markdown +GIF is a bitmap image format. + +H2O + +Xn + Yn = Zn + +Press CTRL + ALT + Delete to end the session. + +Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. +``` + +### Output + +GIF is a bitmap image format. + +H2O + +Xn + Yn = Zn + +Press CTRL + ALT + Delete to end the session. + +Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. diff --git a/src/content/posts/de/second-post.md b/src/content/posts/de/second-post.md new file mode 100644 index 0000000..637a1e2 --- /dev/null +++ b/src/content/posts/de/second-post.md @@ -0,0 +1,17 @@ +--- +title: 'Second post' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 15 2022' +heroImage: '../../../assets/blog-placeholder-4.jpg' +category: de/allgemein +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/src/content/posts/de/setting-up-forgejo-actions-runner.md b/src/content/posts/de/setting-up-forgejo-actions-runner.md new file mode 100644 index 0000000..5767861 --- /dev/null +++ b/src/content/posts/de/setting-up-forgejo-actions-runner.md @@ -0,0 +1,237 @@ +--- +title: 'Forgejo Actions Runner für self-hosted CI/CD einrichten' +description: 'Wie ich manuelle SSH-Deploys durch eine Push-to-Deploy-Pipeline mit einem self-hosted Forgejo Actions Runner auf demselben VPS ersetzt habe.' +pubDate: 'Apr 22 2026' +heroImage: '../../../assets/blog-placeholder-2.jpg' +category: de/technik +tags: + - forgejo + - ci + - self-hosted + - devops + - podman +translationKey: forgejo-actions-runner +--- + +Nachdem ich meine Git-Repos von GitHub auf eine self-hosted Forgejo-Instanz umgezogen hatte, war der nächste logische Schritt, das Deployment von meinem Laptop wegzubekommen. Statt lokal `./scripts/deploy.sh` auszuführen und zu hoffen, dass nichts uncommittet ist, sollte `git push` den Build anstoßen und den Container automatisch ausrollen. + +Dieser Beitrag dokumentiert das komplette Setup: Forgejo Actions Runner auf demselben VPS installieren, an einen Workflow koppeln und Secrets sauber aus dem Repo halten. + +## Das Setup + +- **VPS**: eine Debian-Maschine, die sowohl Forgejo (rootless Podman-Container) als auch die Astro-Website (`/opt/websites/adrian-altner.de`, verwaltet über einen `podman-compose@` systemd-Service) hostet. +- **Forgejo**: v11 LTS, rootless, läuft unter einem eigenen `git` System-User. +- **Ziel**: bei jedem Push auf `main` das Production-Image neu bauen und den Service neu starten — alles auf derselben Maschine. + +## Warum ein eigener Runner-User + +Der Runner führt beliebigen Code aus Workflow-Dateien aus. Ihn als `git`-User laufen zu lassen (der Zugriff auf Forgejos Datenbank und jedes Repo hat) wäre keine gute Idee. Ich habe einen separaten System-User mit abgeschottetem Home-Verzeichnis angelegt: + +```bash +sudo useradd --system --create-home \ + --home-dir /var/lib/forgejo-runner \ + --shell /bin/bash forgejo-runner +``` + +Dieser User bekommt standardmäßig kein sudo — wir erteilen gezielt nur die Rechte, die der Deploy tatsächlich braucht. + +## Runner-Binary installieren + +Der Runner wird als einzelnes statisches Binary aus Forgejos eigener Registry verteilt. Ich hole mir das neueste Release programmatisch: + +```bash +LATEST=$(curl -s https://code.forgejo.org/api/v1/repos/forgejo/runner/releases \ + | grep -oE '"tag_name":"[^"]+"' | head -1 | cut -d'"' -f4) +VER="${LATEST#v}" + +cd /tmp +curl -L -o forgejo-runner \ + "https://code.forgejo.org/forgejo/runner/releases/download/${LATEST}/forgejo-runner-${VER}-linux-amd64" +chmod +x forgejo-runner +sudo mv forgejo-runner /usr/local/bin/ +``` + +Ein kurzes `forgejo-runner --version` bestätigt v12.9.0 — die aktuelle Major-Version, kompatibel mit Forgejo v10, v11 und allem darüber. + +## Actions in Forgejo aktivieren + +Actions sind bei einer Forgejo-Instanz standardmäßig aus. Die minimale Konfiguration kommt in die `app.ini` (bei mir im rootless-Container-Volume unter `/home/git/forgejo-data/custom/conf/app.ini`): + +```ini +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://code.forgejo.org +``` + +`DEFAULT_ACTIONS_URL` ist wichtig, weil der GitHub Actions Marketplace nicht direkt erreichbar ist — Forgejo pflegt eigene Mirrors der gängigen Actions wie `actions/checkout` unter `code.forgejo.org/actions/*`. Nach einem Container-Restart taucht das Verzeichnis `actions_artifacts` in den Logs auf. + +## Runner registrieren + +Runner können auf ein einzelnes Repo, einen User-Account oder die gesamte Instanz registriert werden. Ich habe mit einer Repo-Registrierung für meine Website angefangen und dann auf User-Scope umgestellt, damit derselbe Runner alle meine Repos bedienen kann, ohne sich neu registrieren zu müssen. + +Der Registrierungstoken kommt aus `Benutzer-Einstellungen → Actions → Runner → Neuen Runner erstellen`: + +```bash +sudo -iu forgejo-runner /usr/local/bin/forgejo-runner register \ + --no-interactive \ + --instance https://git.altner.cloud \ + --token \ + --name arcturus-runner \ + --labels "self-hosted:host" +``` + +Das Label `self-hosted:host` bedeutet: "Jobs mit Label `self-hosted` laufen direkt auf dem Host". Kein Container-Runtime für den Runner selbst nötig — Podman haben wir ja schon für die Anwendung. + +Umstellung eines bestehenden Runners von Repo- auf User-Scope: Service stoppen, alten Runner-Eintrag in der Forgejo-UI löschen, `/var/lib/forgejo-runner/.runner` lokal entfernen, neuen User-Level-Token holen, neu registrieren, Service starten. Gleiches Binary, anderer Scope. + +## Docker-Abhängigkeit abschalten + +Beim ersten Start hat sich der Runner geweigert zu laufen: + +``` +Error: daemon Docker Engine socket not found and docker_host config was invalid +``` + +Auch wenn man nur das Host-Label nutzt, prüft der Runner beim Start auf einen Docker-Socket. Da der Server nur rootless Podman hat, habe ich eine Config-Datei erzeugt und den Docker-Check explizit deaktiviert: + +```bash +sudo -iu forgejo-runner /usr/local/bin/forgejo-runner generate-config \ + > /tmp/runner-config.yaml +sudo mv /tmp/runner-config.yaml /var/lib/forgejo-runner/config.yaml +sudo chown forgejo-runner:forgejo-runner /var/lib/forgejo-runner/config.yaml + +sudo -iu forgejo-runner sed -i \ + -e 's|docker_host: .*|docker_host: "-"|' \ + -e 's| labels: \[\]| labels: ["self-hosted:host"]|' \ + /var/lib/forgejo-runner/config.yaml +``` + +## Systemd-Service + +```ini +[Unit] +Description=Forgejo Actions Runner +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=forgejo-runner +Group=forgejo-runner +WorkingDirectory=/var/lib/forgejo-runner +ExecStart=/usr/local/bin/forgejo-runner --config /var/lib/forgejo-runner/config.yaml daemon +Restart=on-failure +RestartSec=5s +NoNewPrivileges=false +ProtectSystem=full +ProtectHome=read-only +ReadWritePaths=/var/lib/forgejo-runner + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now forgejo-runner +``` + +## Nur die nötigen sudo-Rechte + +Der Deploy-Step muss ein Podman-Image bauen und den systemd-Service neu starten, der es ausführt. Beides braucht Root. Statt dem Runner-User breites sudo zu geben, habe ich eine eng gefasste Allowlist unter `/etc/sudoers.d/forgejo-runner-deploy` angelegt: + +``` +forgejo-runner ALL=(root) NOPASSWD: /usr/bin/podman build *, \ + /usr/bin/podman container prune *, \ + /usr/bin/podman image prune *, \ + /usr/bin/podman builder prune *, \ + /usr/bin/systemctl restart podman-compose@adrian-altner.de.service, \ + /usr/bin/rsync * +``` + +`visudo -cf` prüft die Syntax, bevor man sich versehentlich komplett aus sudo aussperrt. + +## Der Workflow + +Workflows liegen unter `.forgejo/workflows/*.yml`. Der Deploy-Flow macht dasselbe wie mein altes Shell-Skript, nur ohne SSH: + +```yaml +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: self-hosted + env: + DEPLOY_DIR: /opt/websites/adrian-altner.de + steps: + - uses: actions/checkout@v4 + + - name: Sync to deploy directory + run: | + sudo rsync -a --delete \ + --exclude='.env' \ + --exclude='.env.production' \ + --exclude='.git/' \ + --exclude='node_modules/' \ + ./ "${DEPLOY_DIR}/" + + - name: Build image + run: | + cd "${DEPLOY_DIR}" + sudo podman build \ + --build-arg WEBMENTION_TOKEN="${{ secrets.WEBMENTION_TOKEN }}" \ + -t localhost/adrian-altner.de:latest . + + - name: Restart service + run: sudo systemctl restart podman-compose@adrian-altner.de.service + + - name: Prune + run: | + sudo podman container prune -f 2>/dev/null || true + sudo podman image prune --external -f 2>/dev/null || true + sudo podman image prune -f 2>/dev/null || true + sudo podman builder prune -af 2>/dev/null || true +``` + +## Secrets bleiben in Forgejo + +Alles Sensible — in meinem Fall API-Tokens für webmention.io und webmention.app — liegt in `Settings → Actions → Secrets` und wird als `${{ secrets.NAME }}` in den Job injiziert. Forgejo speichert sie verschlüsselt, und Workflow-Logs maskieren die Werte automatisch. Die Tokens werden an genau zwei Stellen referenziert: in der CI-Workflow-Datei (committet) und im verschlüsselten Forgejo-Store (nie im Repo). + +Der Build-Time-Token wird als `ARG` in den Container gereicht, nur während des Build-Stages benutzt und ist im finalen Runtime-Image nicht enthalten — ein schnelles `podman run --rm env | grep -i webmention` bestätigt das. + +## Der eine Stolperstein: Node auf dem Host + +Der erste echte Workflow-Lauf ist sofort gestorben mit: + +``` +Cannot find: node in PATH +``` + +`actions/checkout@v4` ist eine JavaScript-basierte Action. Bei einem Runner mit Host-Label läuft sie direkt auf dem VPS und braucht einen Node-Interpreter im `PATH`. Ein `apt install` später war der Runner zufrieden: + +```bash +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt-get install -y nodejs +sudo systemctl restart forgejo-runner +``` + +## Ergebnis + +Von einem kalten `git push origin main` bis zur komplett durchgelaufenen Pipeline — Checkout, rsync, Podman-Build, systemd-Restart, Prune, Webmention-Pings — vergehen etwa 1 Minute 15 Sekunden. Keine SSH-Keys zu rotieren, kein Laptop involviert, kein Mysterium über den Stand der Live-Version. + +Der Runner selbst belegt im Idle rund 5 MB RAM und pollt Forgejo alle zwei Sekunden auf neue Jobs. Der Ressourcen-Overhead ist vernachlässigbar verglichen mit dem Komfort von Push-to-Deploy auf Infrastruktur, die mir komplett gehört. + +## Runner für neue Projekte wiederverwenden + +Weil der Runner auf User-Scope registriert ist, reduziert sich das Anhängen von CI an ein neues Repo auf drei Schritte: + +1. Eine `.forgejo/workflows/deploy.yml` mit `runs-on: self-hosted` ins Repo packen. +2. Projekt-spezifische Secrets unter den Actions-Settings des Repos anlegen. +3. Falls das Projekt einen eigenen systemd-Service hat, `/etc/sudoers.d/forgejo-runner-deploy` um eine Zeile `systemctl restart ` erweitern. Sonst muss auf dem Server nichts geändert werden. + +Die einmaligen Infrastrukturkosten — User-Account, Binary, Config, systemd-Unit, Node-Runtime, sudoers — amortisieren sich über jedes weitere Projekt. diff --git a/src/content/posts/de/third-post.md b/src/content/posts/de/third-post.md new file mode 100644 index 0000000..2f24af7 --- /dev/null +++ b/src/content/posts/de/third-post.md @@ -0,0 +1,17 @@ +--- +title: 'Third post' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 22 2022' +heroImage: '../../../assets/blog-placeholder-2.jpg' +category: de/allgemein +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/src/content/posts/de/using-mdx.mdx b/src/content/posts/de/using-mdx.mdx new file mode 100644 index 0000000..f615ef2 --- /dev/null +++ b/src/content/posts/de/using-mdx.mdx @@ -0,0 +1,35 @@ +--- +title: 'Using MDX' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jun 01 2024' +heroImage: '../../../assets/blog-placeholder-5.jpg' +category: de/technik +tags: + - markdown + - astro +--- + +This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file. + +## Why MDX? + +MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/integrations-guide/mdx/#mdx-in-astro) for things like interactive charts or alerts. + +If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze. + +## Example + +Here is how you import and use a UI component inside of MDX. +When you open this page in the browser, you should see the clickable button below. + +import HeaderLink from '~/components/HeaderLink.astro'; + + + Embedded component in MDX + + +## More Links + +- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx) +- [Astro Usage Documentation](https://docs.astro.build/en/basics/astro-pages/#markdownmdx-pages) +- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default. diff --git a/src/content/posts/en/hello-world.md b/src/content/posts/en/hello-world.md new file mode 100644 index 0000000..6f19250 --- /dev/null +++ b/src/content/posts/en/hello-world.md @@ -0,0 +1,12 @@ +--- +title: 'Hello World' +description: 'First English post.' +pubDate: 'Apr 20 2026' +heroImage: '../../../assets/blog-placeholder-1.jpg' +category: en/general +tags: + - markdown +translationKey: hello-world +--- + +This is the first English post. diff --git a/src/content/posts/en/setting-up-forgejo-actions-runner.md b/src/content/posts/en/setting-up-forgejo-actions-runner.md new file mode 100644 index 0000000..a9edc7d --- /dev/null +++ b/src/content/posts/en/setting-up-forgejo-actions-runner.md @@ -0,0 +1,237 @@ +--- +title: 'Setting up a Forgejo Actions runner for self-hosted CI/CD' +description: 'How I replaced manual SSH deploys with a push-to-deploy pipeline using a self-hosted Forgejo Actions runner on the same VPS.' +pubDate: 'Apr 22 2026' +heroImage: '../../../assets/blog-placeholder-2.jpg' +category: en/tech +tags: + - forgejo + - ci + - self-hosted + - devops + - podman +translationKey: forgejo-actions-runner +--- + +After moving my Git repositories from GitHub to a self-hosted Forgejo instance, the next logical step was to move deployment off my laptop. Instead of running `./scripts/deploy.sh` locally and hoping nothing was uncommitted, I wanted `git push` to trigger the build and roll out the container automatically. + +This post documents the full setup: installing a Forgejo Actions runner on the same VPS that runs Forgejo, wiring it to a workflow, and keeping secrets out of the repo. + +## The setup + +- **VPS**: single Debian machine hosting both Forgejo (rootless Podman container) and the Astro website (`/opt/websites/adrian-altner.de`, managed by a `podman-compose@` systemd service). +- **Forgejo**: v11 LTS, rootless, running under a dedicated `git` system user. +- **Goal**: on every push to `main`, rebuild the production image and restart the service — all on the same box. + +## Why a dedicated runner user + +The runner executes arbitrary code defined in workflow files. Running it as the `git` user (which has access to Forgejo's database and every repo) would be a bad idea. I created a separate system user with a locked-down home directory: + +```bash +sudo useradd --system --create-home \ + --home-dir /var/lib/forgejo-runner \ + --shell /bin/bash forgejo-runner +``` + +That user gets no sudo by default — we'll grant targeted privileges later only for the specific commands the deploy needs. + +## Installing the runner binary + +The runner is distributed as a single static binary from Forgejo's own registry. I grabbed the latest release programmatically: + +```bash +LATEST=$(curl -s https://code.forgejo.org/api/v1/repos/forgejo/runner/releases \ + | grep -oE '"tag_name":"[^"]+"' | head -1 | cut -d'"' -f4) +VER="${LATEST#v}" + +cd /tmp +curl -L -o forgejo-runner \ + "https://code.forgejo.org/forgejo/runner/releases/download/${LATEST}/forgejo-runner-${VER}-linux-amd64" +chmod +x forgejo-runner +sudo mv forgejo-runner /usr/local/bin/ +``` + +A quick `forgejo-runner --version` confirmed v12.9.0 was in place — which is the current major, compatible with Forgejo v10, v11, and beyond. + +## Enabling Actions in Forgejo + +Actions are off by default on Forgejo instances. I added the minimal configuration to `app.ini` (found inside the rootless container's volume at `/home/git/forgejo-data/custom/conf/app.ini`): + +```ini +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://code.forgejo.org +``` + +`DEFAULT_ACTIONS_URL` matters because GitHub's Actions marketplace isn't reachable as-is — Forgejo maintains its own mirrors of common actions like `actions/checkout` at `code.forgejo.org/actions/*`. A container restart and the `actions_artifacts` storage directory appeared in the logs. + +## Registering the runner + +Runners can be scoped to a single repo, to a user account, or to the whole instance. I started with a repo-scoped registration for my website, then moved it to user-scope so the same runner can serve every repo I own without re-registration. + +The registration token came from `User Settings → Actions → Runners → Create new Runner`: + +```bash +sudo -iu forgejo-runner /usr/local/bin/forgejo-runner register \ + --no-interactive \ + --instance https://git.altner.cloud \ + --token \ + --name arcturus-runner \ + --labels "self-hosted:host" +``` + +The label `self-hosted:host` means "jobs labelled `self-hosted` run directly on the host". No container runtime required for the runner itself — we already have Podman for the application. + +To switch an existing runner from repo to user scope: stop the service, delete the old runner entry in the Forgejo UI, remove `/var/lib/forgejo-runner/.runner` locally, grab a new user-level token, re-register, start the service. Same binary, different scope. + +## Making it not-need-Docker + +On first boot, the runner refused to start with: + +``` +Error: daemon Docker Engine socket not found and docker_host config was invalid +``` + +Even when using only the host label, the runner checks for a Docker socket on startup. Since the server only has rootless Podman, I generated a config file and explicitly disabled the Docker check: + +```bash +sudo -iu forgejo-runner /usr/local/bin/forgejo-runner generate-config \ + > /tmp/runner-config.yaml +sudo mv /tmp/runner-config.yaml /var/lib/forgejo-runner/config.yaml +sudo chown forgejo-runner:forgejo-runner /var/lib/forgejo-runner/config.yaml + +sudo -iu forgejo-runner sed -i \ + -e 's|docker_host: .*|docker_host: "-"|' \ + -e 's| labels: \[\]| labels: ["self-hosted:host"]|' \ + /var/lib/forgejo-runner/config.yaml +``` + +## Systemd service + +```ini +[Unit] +Description=Forgejo Actions Runner +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=forgejo-runner +Group=forgejo-runner +WorkingDirectory=/var/lib/forgejo-runner +ExecStart=/usr/local/bin/forgejo-runner --config /var/lib/forgejo-runner/config.yaml daemon +Restart=on-failure +RestartSec=5s +NoNewPrivileges=false +ProtectSystem=full +ProtectHome=read-only +ReadWritePaths=/var/lib/forgejo-runner + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now forgejo-runner +``` + +## Granting just enough sudo + +The deploy step needs to build a Podman image and restart the systemd service that runs it. Both require root. Instead of giving the runner user broad sudo, I created a narrow allowlist in `/etc/sudoers.d/forgejo-runner-deploy`: + +``` +forgejo-runner ALL=(root) NOPASSWD: /usr/bin/podman build *, \ + /usr/bin/podman container prune *, \ + /usr/bin/podman image prune *, \ + /usr/bin/podman builder prune *, \ + /usr/bin/systemctl restart podman-compose@adrian-altner.de.service, \ + /usr/bin/rsync * +``` + +`visudo -cf` parses it to catch syntax errors before you accidentally lock yourself out of sudo entirely. + +## The workflow + +Workflows live under `.forgejo/workflows/*.yml`. The deploy flow mirrors what my old shell script did, minus the SSH: + +```yaml +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: self-hosted + env: + DEPLOY_DIR: /opt/websites/adrian-altner.de + steps: + - uses: actions/checkout@v4 + + - name: Sync to deploy directory + run: | + sudo rsync -a --delete \ + --exclude='.env' \ + --exclude='.env.production' \ + --exclude='.git/' \ + --exclude='node_modules/' \ + ./ "${DEPLOY_DIR}/" + + - name: Build image + run: | + cd "${DEPLOY_DIR}" + sudo podman build \ + --build-arg WEBMENTION_TOKEN="${{ secrets.WEBMENTION_TOKEN }}" \ + -t localhost/adrian-altner.de:latest . + + - name: Restart service + run: sudo systemctl restart podman-compose@adrian-altner.de.service + + - name: Prune + run: | + sudo podman container prune -f 2>/dev/null || true + sudo podman image prune --external -f 2>/dev/null || true + sudo podman image prune -f 2>/dev/null || true + sudo podman builder prune -af 2>/dev/null || true +``` + +## Secrets stay in Forgejo + +Anything sensitive — API tokens for webmention.io and webmention.app in my case — lives in `Settings → Actions → Secrets` and is injected into the job as `${{ secrets.NAME }}`. Forgejo stores them encrypted, and the workflow logs automatically mask the values. The tokens are referenced from exactly two places: the CI workflow file (committed) and Forgejo's encrypted store (never in the repo). + +The build-time token is passed into the container as an `ARG`, used only during the build stage, and not present in the final runtime image — a quick `podman run --rm env | grep -i webmention` confirms it's gone. + +## The one gotcha: Node on the host + +The first real workflow run failed immediately with: + +``` +Cannot find: node in PATH +``` + +`actions/checkout@v4` is a JavaScript-based action. On a runner using the host label, it runs directly on the VPS and needs a Node interpreter available in `PATH`. One apt install later and the runner was happy: + +```bash +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt-get install -y nodejs +sudo systemctl restart forgejo-runner +``` + +## Result + +From a cold `git push origin main`, the whole pipeline — checkout, rsync, Podman build, systemd restart, prune, Webmention pings — completes in about 1 minute 15 seconds. No SSH keys to rotate, no laptop involved, no mystery about which version of the code is live. + +The runner itself uses about 5 MB of RAM while idle, polling Forgejo every two seconds for new jobs. Resource overhead is negligible compared to the convenience of push-to-deploy on infrastructure I fully control. + +## Reusing the runner for new projects + +Because the runner is registered at user scope, adding CI to a new repository boils down to three steps: + +1. Drop a `.forgejo/workflows/deploy.yml` into the repo with `runs-on: self-hosted`. +2. Add any project-specific secrets under the repo's Actions settings. +3. If the project has its own systemd service, extend `/etc/sudoers.d/forgejo-runner-deploy` with a line allowing `systemctl restart `. Nothing else on the server needs to change. + +The one-time infrastructure cost — user account, binary, config, systemd unit, Node runtime, sudoers — gets amortised across every project from here on. diff --git a/src/i18n/posts.ts b/src/i18n/posts.ts new file mode 100644 index 0000000..2a5bc0b --- /dev/null +++ b/src/i18n/posts.ts @@ -0,0 +1,152 @@ +import { type CollectionEntry, getCollection, getEntry } from 'astro:content'; +import { type Locale } from '~/consts'; +import { isLocale, localizePath } from '~/i18n/ui'; + +export function entryLocale(entry: { id: string }): Locale { + const first = entry.id.split('/')[0]; + if (!isLocale(first)) { + throw new Error(`Content entry "${entry.id}" is not under a locale folder (de/ or en/).`); + } + return first; +} + +export function entrySlug(entry: { id: string }): string { + return entry.id.split('/').slice(1).join('/'); +} + +// Back-compat aliases used across the codebase. +export const postLocale = entryLocale; +export const postSlug = entrySlug; + +export async function getPostsByLocale(locale: Locale) { + const posts = await getCollection('posts', (p) => entryLocale(p) === locale); + return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); +} + +export async function getCategoriesByLocale(locale: Locale) { + const categories = await getCollection('categories', (c) => entryLocale(c) === locale); + return categories.sort((a, b) => a.data.name.localeCompare(b.data.name)); +} + +export async function getPostsByCategory(category: CollectionEntry<'categories'>) { + const locale = entryLocale(category); + const posts = await getCollection( + 'posts', + (p) => entryLocale(p) === locale && p.data.category?.id === category.id, + ); + return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); +} + +/** Convert a tag name into a URL-safe slug. */ +export function tagSlug(tag: string): string { + return tag + .toLowerCase() + .trim() + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/ß/g, 'ss') + .replace(/ä/g, 'ae') + .replace(/ö/g, 'oe') + .replace(/ü/g, 'ue') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +/** Aggregated tag info across posts in one locale. */ +export interface TagEntry { + name: string; + slug: string; + count: number; +} + +export async function getTagsByLocale(locale: Locale): Promise { + const posts = await getPostsByLocale(locale); + const byName = new Map(); + for (const post of posts) { + for (const raw of post.data.tags ?? []) { + const name = raw.trim(); + if (!name) continue; + const existing = byName.get(name); + if (existing) existing.count++; + else byName.set(name, { name, slug: tagSlug(name), count: 1 }); + } + } + return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); +} + +/** Resolve a tag slug for a locale back to its canonical TagEntry. */ +export async function findTagBySlug(locale: Locale, slug: string): Promise { + const tags = await getTagsByLocale(locale); + return tags.find((t) => t.slug === slug); +} + +export async function getPostsByTag(locale: Locale, slug: string) { + const posts = await getPostsByLocale(locale); + return posts.filter((p) => (p.data.tags ?? []).some((name) => tagSlug(name) === slug)); +} + +export async function resolveCategory(post: CollectionEntry<'posts'>) { + if (!post.data.category) return undefined; + return await getEntry(post.data.category); +} + +/** URL segment used for category detail pages per locale. */ +export function categorySegment(locale: Locale): string { + return locale === 'de' ? 'kategorie' : 'category'; +} + +/** URL segment used for the category listing page per locale. */ +export function categoryIndexSegment(locale: Locale): string { + return locale === 'de' ? 'kategorien' : 'categories'; +} + +/** URL segment used for the about page per locale. */ +export function aboutSegment(locale: Locale): string { + return locale === 'de' ? 'ueber-mich' : 'about'; +} + +/** URL segment used for tag detail pages per locale. */ +export function tagSegment(locale: Locale): string { + return locale === 'de' ? 'schlagwort' : 'tag'; +} + +/** URL segment used for the tag listing page per locale. */ +export function tagIndexSegment(locale: Locale): string { + return locale === 'de' ? 'schlagwoerter' : 'tags'; +} + +export function categoryHref(category: CollectionEntry<'categories'>): string { + const locale = entryLocale(category); + return localizePath(`/${categorySegment(locale)}/${entrySlug(category)}/`, locale); +} + +export function postHref(post: CollectionEntry<'posts'>): string { + const locale = entryLocale(post); + return localizePath(`/${entrySlug(post)}/`, locale); +} + +export function tagHref(locale: Locale, tag: string | TagEntry): string { + const slug = typeof tag === 'string' ? tagSlug(tag) : tag.slug; + return localizePath(`/${tagSegment(locale)}/${slug}/`, locale); +} + +/** Canonical URL for any translatable entry. */ +export function entryHref(entry: CollectionEntry<'posts' | 'categories'>): string { + return entry.collection === 'categories' ? categoryHref(entry) : postHref(entry); +} + +/** + * Find the translation of an entry in the target locale, matched via the + * shared `translationKey` frontmatter field. Returns `undefined` when no + * matching translation exists. + */ +export async function findTranslation( + entry: CollectionEntry<'posts' | 'categories'>, + target: Locale, +): Promise | undefined> { + const key = entry.data.translationKey; + if (!key) return undefined; + const collection = entry.collection; + const all = await getCollection(collection, (e) => entryLocale(e) === target); + return all.find((e) => e.data.translationKey === key); +} diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts new file mode 100644 index 0000000..038b321 --- /dev/null +++ b/src/i18n/ui.ts @@ -0,0 +1,134 @@ +import { DEFAULT_LOCALE, type Locale, LOCALES } from '~/consts'; + +export const ui = { + de: { + 'nav.home': 'Start', + 'nav.about': 'Über mich', + 'nav.categories': 'Kategorien', + 'nav.tags': 'Schlagwörter', + 'post.lastUpdated': 'Zuletzt aktualisiert am', + 'post.category': 'Kategorie', + 'post.tags': 'Schlagwörter', + 'post.translationAvailable': 'Dieser Beitrag ist auch auf Englisch verfügbar:', + 'post.translationLink': 'Englische Version lesen', + 'categories.title': 'Kategorien', + 'categories.description': 'Alle Kategorien im Überblick.', + 'category.postsIn': 'Beiträge in', + 'category.noPosts': 'Noch keine Beiträge in dieser Kategorie.', + 'tags.title': 'Schlagwörter', + 'tags.description': 'Alle Schlagwörter im Überblick.', + 'tag.postsTagged': 'Beiträge mit', + 'tag.noPosts': 'Noch keine Beiträge mit diesem Stichwort.', + 'footer.contact': 'Kontakt', + 'footer.imprint': 'Impressum', + 'footer.privacy': 'Datenschutz', + 'webmentions.title': 'Reaktionen', + 'webmentions.like': 'Like', + 'webmentions.likes': 'Likes', + 'webmentions.repost': 'Repost', + 'webmentions.reposts': 'Reposts', + 'webmentions.replies': 'Antworten', + 'webmentions.mentions': 'Erwähnungen', + 'lang.de': 'Deutsch', + 'lang.en': 'English', + }, + en: { + 'nav.home': 'Home', + 'nav.about': 'About', + 'nav.categories': 'Categories', + 'nav.tags': 'Tags', + 'post.lastUpdated': 'Last updated on', + 'post.category': 'Category', + 'post.tags': 'Tags', + 'post.translationAvailable': 'This post is also available in German:', + 'post.translationLink': 'Read the German version', + 'categories.title': 'Categories', + 'categories.description': 'All categories at a glance.', + 'category.postsIn': 'Posts in', + 'category.noPosts': 'No posts in this category yet.', + 'tags.title': 'Tags', + 'tags.description': 'All tags at a glance.', + 'tag.postsTagged': 'Posts tagged', + 'tag.noPosts': 'No posts with this tag yet.', + 'footer.contact': 'Contact', + 'footer.imprint': 'Imprint', + 'footer.privacy': 'Privacy', + 'webmentions.title': 'Reactions', + 'webmentions.like': 'Like', + 'webmentions.likes': 'Likes', + 'webmentions.repost': 'Repost', + 'webmentions.reposts': 'Reposts', + 'webmentions.replies': 'Replies', + 'webmentions.mentions': 'Mentions', + 'lang.de': 'Deutsch', + 'lang.en': 'English', + }, +} as const satisfies Record>; + +export type UIKey = keyof (typeof ui)['de']; + +export function t(locale: Locale, key: UIKey): string { + return ui[locale][key]; +} + +export function isLocale(value: string | undefined): value is Locale { + return !!value && (LOCALES as readonly string[]).includes(value); +} + +export function getLocaleFromUrl(url: URL): Locale { + const seg = url.pathname.split('/').filter(Boolean)[0]; + return isLocale(seg) ? seg : DEFAULT_LOCALE; +} + +/** + * Build a URL for a route within a given locale. `path` is the route without + * any language prefix, e.g. "/" or "/about". + */ +export function localizePath(path: string, locale: Locale): string { + const normalized = path.startsWith('/') ? path : `/${path}`; + if (locale === DEFAULT_LOCALE) return normalized; + if (normalized === '/') return `/${locale}/`; + return `/${locale}${normalized}`; +} + +/** + * Segments whose URL slug differs per locale. The first segment of any + * non-prefixed pathname is translated through this map when switching. + */ +const LOCALIZED_SEGMENTS: Record> = { + de: { + category: 'kategorie', + categories: 'kategorien', + about: 'ueber-mich', + tag: 'schlagwort', + tags: 'schlagwoerter', + contact: 'kontakt', + imprint: 'impressum', + 'privacy-policy': 'datenschutz', + }, + en: { + kategorie: 'category', + kategorien: 'categories', + 'ueber-mich': 'about', + schlagwort: 'tag', + schlagwoerter: 'tags', + kontakt: 'contact', + impressum: 'imprint', + datenschutz: 'privacy-policy', + }, +}; + +/** + * Swap the locale of the current pathname, preserving the rest of the route + * and translating known per-locale URL segments (e.g. `kategorie` ↔ `category`). + */ +export function switchLocalePath(pathname: string, target: Locale): string { + const parts = pathname.split('/').filter(Boolean); + if (parts.length > 0 && isLocale(parts[0])) parts.shift(); + if (parts.length > 0) { + const translated = LOCALIZED_SEGMENTS[target][parts[0]]; + if (translated) parts[0] = translated; + } + const rest = parts.length ? `/${parts.join('/')}` : '/'; + return localizePath(rest === '/' ? '/' : rest, target); +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..ce7227c --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,57 @@ +--- +import type { ImageMetadata } from 'astro'; +import type { CollectionEntry } from 'astro:content'; +import BaseHead from '~/components/BaseHead.astro'; +import Footer from '~/components/Footer.astro'; +import Header from '~/components/Header.astro'; +import { DEFAULT_LOCALE, type Locale } from '~/consts'; +import { getLocaleFromUrl } from '~/i18n/ui'; + +interface Props { + title: string; + description: string; + locale?: Locale; + image?: ImageMetadata; + /** Current content entry, used for the language switcher's translation lookup. */ + entry?: CollectionEntry<'posts' | 'categories'>; + /** Optional extra class on `` for per-page styling hooks. */ + bodyClass?: string; +} + +const { + title, + description, + image, + entry, + bodyClass, + locale = getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE, +} = Astro.props; +--- + + + + + + + + + +
+
+ +
+