diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0dd47824..9b3ef560 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -4,8 +4,12 @@ on: push: branches: - master - # Review gh actions docs if you want to further define triggers, paths, etc - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + # Daily rebuild + manual trigger — refreshes build-time data like the GitHub + # star count in site/src/data/github.ts without a manual deploy. + schedule: + - cron: '0 6 * * *' + # Allow manual runs from the Actions tab. + workflow_dispatch: jobs: deploy: @@ -17,14 +21,17 @@ jobs: with: node-version: 24 cache: npm - cache-dependency-path: website/package-lock.json + cache-dependency-path: site/package-lock.json - name: Install dependencies run: npm ci - working-directory: website + working-directory: site - name: Build website run: npm run build - working-directory: website + working-directory: site + env: + # Lifts the GitHub API rate limit for build-time stats fetches. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Popular action to deploy to GitHub Pages: # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus @@ -33,7 +40,7 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} # Build output to publish to the `gh-pages` branch: - publish_dir: ./website/build + publish_dir: ./site/dist # The following lines assign commit authorship to the official # GH-Actions bot for deploys to `gh-pages` branch: # https://github.com/actions/checkout/issues/13#issuecomment-724415212 diff --git a/.gitignore b/.gitignore index b31785db..ae35effc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ vendor .idea .hugo_build.lock +.superpowers/ +.serena/ +.playwright-mcp/ diff --git a/docs/superpowers/specs/2026-06-14-echo-docs-astro-rebuild-design.md b/docs/superpowers/specs/2026-06-14-echo-docs-astro-rebuild-design.md new file mode 100644 index 00000000..1cd79832 --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-echo-docs-astro-rebuild-design.md @@ -0,0 +1,136 @@ +# Echo Docs — rebuild on Astro Starlight ("Terminal" theme) + +**Date:** 2026-06-14 (rev. 2 — post-review) +**Status:** Design — hardened after architecture + docs-platform review +**Repo:** `labstack/echox` (docs site in `website/`), branch `docs-astro-rebuild` + +## 1. Goal + +Replace the current Docusaurus docs with a professional, `docs.openclaw.ai`-grade site +branded for Echo. The previous attempts read as "default/amateur" because of generic +system-sans on a cool palette; the new design commits to a **terminal-precise** aesthetic. + +## 2. Locked decisions (rev. 2) + +| Decision | Choice | Notes | +|---|---|---| +| Platform | **Astro Starlight** (base) + custom Terminal theme | *Changed from full-custom Astro after review.* Starlight gives sidebar, i18n+fallback, Pagefind search, prev/next, TOC, edit links, last-updated, per-page SEO/OG, sitemap, 404, theme toggle, mobile drawer, a11y baseline — we override the theme for the look. Far less to build/own. | +| Design direction | **Terminal** | Locked via mockups (`refine-terminal.html`, `home.html`). | +| Fonts | **DM Sans** for prose & headings · **Fragment Mono** for code + UI chrome (nav, sidebar, labels, ⌘K) | *Changed from mono-everywhere* for long-form readability; terminal feel kept in chrome/code. | +| Palette | warm near-black `#0d0b0b`, warm grays, **Echo cyan** | Cyan from logo; **contrast-tuned** (see §6). | +| Theme | dark-first + light | Starlight toggle; dark default. | +| Content | rewritten fresh, **seeded by porting existing pages** | Avoid blank-page risk on 61 existing pages (review). | +| Launch versioning | **v5 only**; v4 added later via `starlight-versions` | Defers Starlight's one weak area. | +| Search + Ask Echo | **unified ⌘K** with Search (Pagefind) + Ask tabs | Override Starlight `Search` component. | +| Deploy | **Cloudflare Pages** (+ `_redirects`) | Fast, Pagefind-friendly, native 301s. | + +## 3. What Starlight gives us vs. what we build + +- **Starlight (free):** content collections, sidebar config, prev/next, TOC, Pagefind ⌘K, + i18n routing + en-fallback, per-page ``/description/canonical/OG/Twitter, sitemap, + `404`, edit-on-GitHub (`editLink`), `lastUpdated` from git, dark/light toggle + persistence, + mobile drawer, keyboard-accessible nav. +- **We build/own:** + 1. **Terminal theme** — `src/styles/terminal.css` (Starlight CSS custom props + targeted + overrides), Shiki theme JSON, font wiring (DM Sans prose / Fragment Mono chrome+code). + 2. **Unified ⌘K** — override `components.Search` with `CommandPalette.tsx` (Pagefind tab + Ask tab). + 3. **Ask Echo** — provider-pluggable island; stub stream now, `askProvider(query, locale)` hook + for kapa.ai / Inkeep + key later. + 4. **DocActions** — Copy page / Open in ChatGPT / Open in Claude (Edit + Last-updated already from Starlight). + 5. **Homepage** — custom `index.astro` (Starlight `splash` template or standalone): split hero + + code window + stats + feature grid. + 6. **Redirect map, analytics, contrast/a11y tuning, cutover** (§6, §7). + +## 4. Architecture + +``` +website/ + astro.config.mjs # starlight() integration: title, logo, social, editLink, + # lastUpdated, sidebar, locales, components overrides, sitemap + src/ + content/docs/ # MDX content (Starlight collection) + index.mdx # homepage (splash) OR pages/index.astro + <locale>/core/routing.mdx ... # en authored first + components/ # Starlight component overrides + Search.astro # mounts CommandPalette island (unified ⌘K) + CommandPalette.tsx # island: Search (Pagefind JS API) + Ask Echo tabs + DocActions.astro # copy / ChatGPT / Claude toolbar (in PageFrame or content) + styles/ + terminal.css # the design system (tokens + overrides) + shiki/echo-terminal.json # custom Shiki theme (warm bg, cyan keywords) + public/ + logo-*.svg og-*.png robots.txt _redirects # Cloudflare 301 map +``` + +**Isolation:** the theme is pure CSS over Starlight's documented custom properties → no fork +of Starlight internals; only two component overrides (`Search`, optionally `Head`/`PageFrame` +for DocActions). `CommandPalette.tsx` and `AskEcho` logic are self-contained islands. Content +is the single source of truth; everything else is configuration + theme. + +## 5. Design system (`terminal.css`) + +- **Fonts:** `--sl-font: "DM Sans", ui-sans-serif, system-ui, sans-serif` (prose+headings); + `--sl-font-mono: "Fragment Mono", ui-monospace, SFMono-Regular, Menlo, monospace` (code). + Apply Fragment Mono to UI chrome (`.sidebar`, site nav, `.sl-markdown-content` inline UI, + labels, ⌘K) via targeted selectors. **Self-host both fonts** (woff2, subset), `font-display:swap`, + `preload` the prose font (FOUT fix). +- **Color (dark):** bg `#0d0b0b`, surface `#151210`, line `#221e1c`/`#2d2724`, text `#aaa19d`, + heading `#f4f1ef`, muted `#817a76`. Map to Starlight `--sl-color-*`. +- **Accent (contrast-tuned):** use brighter cyan **`#33c9e6`** for small text/links on dark + (≥ ~7:1) and reserve `#00afd1`→`#4ae1ff` for fills/large/decorative — fixes the ~5.9:1 issue. +- Light theme: warm paper `#faf8f7`, ink `#1a1614`, darker cyan for AA. +- Radius 9–13px; hairline borders; one soft cyan glow; faint grain (disabled under + `prefers-reduced-motion`). + +## 6. Production / launch requirements (added per review) + +- **SEO:** rely on Starlight per-page title/description/canonical/OG; ensure every MDX has + `title` + `description` frontmatter. Add **JSON-LD** (`TechArticle` + `BreadcrumbList`) and + `public/robots.txt` → sitemap. Set **v5 as `rel=canonical`** version. +- **Redirects (deliverable):** crawl the live site, produce an explicit **old→new 301 table** + in `public/_redirects`. Cover the 3 alias families (`/guide`, `/cookbook`, `/middleware`), + the one-off redirects, and any old slug with no 1:1 new home (resolve each). No silent drops. +- **Analytics:** carry over **GA4 `G-H19TMZLQFN`** (anonymizeIP) via Starlight `head` inject; + confirm whether to keep or replace. +- **Broken-link gate:** CI step (`lychee`/`astro-broken-link-checker`) that **fails the build**, + matching the old `onBrokenLinks: throw`. +- **Accessibility:** WCAG-AA contrast audit of every token pair; `:focus-visible` rings; + `prefers-reduced-motion` disables glow/grain; the ⌘K palette uses an accessible + combobox pattern (focus trap, `aria-activedescendant`, SR announcements). +- **Responsive:** Starlight handles sidebar/TOC drawers; we ensure the **homepage hero stacks** + (terminal window below headline) on mobile. +- **i18n realism:** keep non-en locales **behind a flag** until real translations exist; add + `hreflang` + `x-default` when they ship; Ask Echo answers carry a `locale` param with an + "answers in English" note where the provider lacks localization. +- **CodeBlock features:** copy button, language label, filename, line-highlighting, and + tabbed examples (Starlight Expressive-Code or our wrapper). + +## 7. Implementation phasing + +- **Phase 1 — Platform + theme + slice (first plan).** Starlight scaffold in `website/` + (replacing Docusaurus), `terminal.css` design system + Shiki theme + self-hosted fonts, + `Search`→`CommandPalette` override with Ask Echo island (stubbed), `DocActions`, custom + **homepage**, contrast/a11y tokens, and a real **content slice** (Quickstart, Routing w/ code, + one nested page). Final `/...` URL/Pagefind/i18n config in place. Dark+light. Shippable to a + **preview** subdomain. +- **Phase 2 — Content.** Author full v5/en docs (Guide, Core, Cookbook, Middleware, API), + seeded by porting+editing existing pages. Site is launch-ready at completion. +- **Phase 3 — Versioning + locales + cutover.** `starlight-versions` for v4, enable locales + + translations, finalize `_redirects`, GA, JSON-LD, broken-link CI, **cutover runbook** + (preview → verify redirects/SEO/search → domain swap on `echo.labstack.com` → keep old + Docusaurus deployable ~2 weeks for rollback). + +## 8. Out of scope (now) + +Live AI provider keys (UI + hook only); real translations (scaffold only); marketing pages +beyond the homepage; v4 content (Phase 3). + +## 9. Risks + +- **Content rewrite is the largest cost** (61 pages × locales) — Phase 2 may need its own + decomposition; mitigate by porting existing prose rather than blank-page rewriting. +- **Starlight theming ceiling:** the Terminal look must be achievable via custom props + + light component overrides; if a surface resists theming, prefer a small component override + over forking Starlight. (Low risk — the look was already achieved via CSS on Docusaurus.) +- **`starlight-versions` maturity** for v4 in Phase 3 — re-evaluate at that point; v5-only + launch de-risks it. diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 00000000..de35ccf3 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.astro/ +.DS_Store diff --git a/site/astro.config.mjs b/site/astro.config.mjs new file mode 100644 index 00000000..20512b87 --- /dev/null +++ b/site/astro.config.mjs @@ -0,0 +1,83 @@ +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; +import { redirects } from './src/redirects.mjs'; + +// https://astro.build/config +export default defineConfig({ + site: 'https://echo.labstack.com', + // Preserve every live Docusaurus /docs/* URL at cutover (generated — see ./src/redirects.mjs). + redirects, + integrations: [ + starlight({ + title: 'Echo', + logo: { + light: './src/assets/logo-light.svg', + dark: './src/assets/logo-dark.svg', + replacesTitle: true, + }, + customCss: ['./src/styles/terminal.css'], + social: [ + { icon: 'github', label: 'GitHub', href: 'https://github.com/labstack/echo' }, + ], + editLink: { + baseUrl: 'https://github.com/labstack/echox/edit/master/site/', + }, + lastUpdated: true, + // Echo "E" cube mark; .ico kept as legacy fallback, apple-touch-icon added in head. + favicon: '/favicon.svg', + // Keep Starlight's built-in Pagefind ⌘K search; add Ask Echo + DocActions via overrides. + components: { + Footer: './src/components/Footer.astro', + PageTitle: './src/components/PageTitle.astro', + }, + head: [ + // Google Analytics (carried over from the Docusaurus site, anonymized IP). + { tag: 'script', attrs: { async: true, src: 'https://www.googletagmanager.com/gtag/js?id=G-H19TMZLQFN' } }, + { + tag: 'script', + content: + "window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-H19TMZLQFN',{anonymize_ip:true});", + }, + // Dark-first: default new visitors to dark unless they've chosen otherwise. + { + tag: 'script', + content: "try{if(!localStorage.getItem('starlight-theme')){localStorage.setItem('starlight-theme','dark');document.documentElement.dataset.theme='dark';}}catch(e){document.documentElement.dataset.theme='dark';}", + }, + { tag: 'link', attrs: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } }, + { tag: 'link', attrs: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: true } }, + { + tag: 'link', + attrs: { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=Fragment+Mono&display=swap', + }, + }, + { + tag: 'link', + attrs: { + rel: 'stylesheet', + href: 'https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css', + }, + }, + // Default social card. Starlight already emits og:title/description/url + // and twitter:card=summary_large_image, but no image — add a site-wide + // default so shares aren't imageless. Absolute URLs are required by + // social scrapers. Per-page overrides can set their own og:image later. + { tag: 'meta', attrs: { property: 'og:image', content: 'https://echo.labstack.com/og.png' } }, + { tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } }, + { tag: 'meta', attrs: { property: 'og:image:height', content: '630' } }, + { tag: 'meta', attrs: { name: 'twitter:image', content: 'https://echo.labstack.com/og.png' } }, + { tag: 'link', attrs: { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' } }, + ], + // Autogenerated from the content dirs — new pages appear automatically, + // ordered by each page's `sidebar.order` frontmatter. + sidebar: [ + { label: 'Guide', items: [{ autogenerate: { directory: 'guide' } }] }, + { label: 'Middleware', items: [{ autogenerate: { directory: 'middleware' } }] }, + { label: 'Cookbook', items: [{ autogenerate: { directory: 'cookbook' } }] }, + ], + // tune the built-in code theme toward our terminal palette + expressiveCode: { themes: ['github-dark', 'github-light'] }, + }), + ], +}); diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 00000000..c68173e8 --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,6876 @@ +{ + "name": "echo-docs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "echo-docs", + "version": "0.1.0", + "dependencies": { + "@astrojs/starlight": "0.40.0", + "astro": "6.4.6", + "sharp": "0.35.1" + } + }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", + "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", + "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "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", + "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": "6.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-6.0.3.tgz", + "integrity": "sha512-+4P3ZvwsRAqAbBgY+uZMewFo3ficlIBPZfu/Luk+v4ia/ZOuFhpsw7r+7672uT2Fc1UPdp7yW0eU5egvSq0wbw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.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": { + "@astrojs/markdown-satteri": "0.3.0", + "astro": "^6.4.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-satteri": { + "optional": true + } + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", + "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.40.0.tgz", + "integrity": "sha512-H1NBIXx4Xw6YzKMsoMkazYxFgnTTj6pD4IReUGWj1fqw82AOAgj+WnZLpTDWRExf3b9ZM7Popbl583i4IvDNVQ==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "^7.2.0", + "@astrojs/mdx": "^6.0.2", + "@astrojs/sitemap": "^3.7.2", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.43.1", + "bcp-47": "^2.1.0", + "hast-util-from-html": "^2.0.3", + "hast-util-select": "^6.0.4", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "i18next": "^26.0.7", + "js-yaml": "^4.1.1", + "klona": "^2.0.6", + "magic-string": "^0.30.21", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "rehype": "^13.0.2", + "rehype-format": "^5.0.1", + "remark-directive": "^4.0.0", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "@astrojs/markdown-satteri": "^0.2.0", + "astro": "^6.4.5" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-satteri": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz", + "integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz", + "integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "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/@expressive-code/core": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.43.1.tgz", + "integrity": "sha512-H4rUJXKyS6y2q9Ig9bIp3dFhWhkZQIeH/jRGl3DROlslrGvfD4OC9qzmvKEFExm+/DtdvvHMQ8/Olmrcfxp+wQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.43.1.tgz", + "integrity": "sha512-tENfLw2UDeq5h749tTLvUtQYvgjIiQc6W7PBCR5xQ4yuE/QftManKJfUQjwJo6RRsAimVQDN4alhFTJ3aq1Khg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.43.1" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.43.1.tgz", + "integrity": "sha512-NdceinYEROXODNgB/ix+7oCdIg+nGyok+E+p2lU9YlWd1xKshXdXpmmptKfkuU27MJ5jjnfhMCI78YYBGi9GtQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.43.1", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.43.1.tgz", + "integrity": "sha512-JWf8wdbZSNoGY4TFv3lmt3/NNDaCP7iYL6rRYD05g8YYjKL62hKUHLl5+B47+v0+bqbuMhXDN7qz2wywFUvMkg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.43.1" + } + }, + "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.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.1.tgz", + "integrity": "sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.1.tgz", + "integrity": "sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.1.tgz", + "integrity": "sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", + "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", + "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", + "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", + "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", + "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", + "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", + "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", + "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "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.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.1.tgz", + "integrity": "sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.1.tgz", + "integrity": "sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.1.tgz", + "integrity": "sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.1.tgz", + "integrity": "sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.1.tgz", + "integrity": "sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.1.tgz", + "integrity": "sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.1.tgz", + "integrity": "sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.1.tgz", + "integrity": "sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.1.tgz", + "integrity": "sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.1.tgz", + "integrity": "sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.1.tgz", + "integrity": "sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.1.tgz", + "integrity": "sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.1.tgz", + "integrity": "sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.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/@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/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "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.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz", + "integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.2.0", + "@shikijs/types": "4.2.0", + "@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.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz", + "integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz", + "integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz", + "integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz", + "integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz", + "integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz", + "integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==", + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "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.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "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.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.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.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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.4.6", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.6.tgz", + "integrity": "sha512-48OBTBKR9ctbf+DQxpOuxGl8ebfn59zTuNQMBzptmG/Mi/H8IdfMSbJgGuX1I/4U6g9yazG1p4BHlf4+2hWU4Q==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@astrojs/telemetry": "3.3.2", + "@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.8.1", + "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", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.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.4", + "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", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.5", + "vfile": "^6.0.3", + "vite": "^7.3.2", + "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/astro-expressive-code": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.43.1.tgz", + "integrity": "sha512-xddgwQxFRwpnnAnU7kSfrO82SsOAq7sQrYpXxVcrN9k/0aqNlTH2+mLrOMm1wXm6jdFKepst3hd8/qWojwuunw==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.43.1" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + } + }, + "node_modules/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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/astro/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", + "optional": true, + "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/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/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "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-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/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.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "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/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/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.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "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-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/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/expressive-code": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.43.1.tgz", + "integrity": "sha512-JdOzanoU825iNvslmk6Kg8Ro61eSHmDK2Zz7BynOxObVrpIXZNzrIZOwQO2uDQcGsjSYShL/8vTrXgeWYnq3NA==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.43.1", + "@expressive-code/plugin-frames": "^0.43.1", + "@expressive-code/plugin-shiki": "^0.43.1", + "@expressive-code/plugin-text-markers": "^0.43.1" + } + }, + "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": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "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/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/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "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-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.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-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^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-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.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-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.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/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/i18next": { + "version": "26.3.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", + "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "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.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@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-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.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-visit-parents": "^6.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-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "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/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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/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.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", + "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "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/pagefind": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" + } + }, + "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/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.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "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/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-expressive-code": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.43.1.tgz", + "integrity": "sha512-CUOGQVlUcSMSXZgpcq9xL6B+dZqnI3w1R6EZj932XpGgj2Hmy7H6oMqa9W/Z7X2HOILWLWhqu1b9kuYcD+nd6w==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.43.1" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.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-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "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.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "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.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz", + "integrity": "sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.1", + "@img/sharp-darwin-x64": "0.35.1", + "@img/sharp-freebsd-wasm32": "0.35.1", + "@img/sharp-libvips-darwin-arm64": "1.3.0", + "@img/sharp-libvips-darwin-x64": "1.3.0", + "@img/sharp-libvips-linux-arm": "1.3.0", + "@img/sharp-libvips-linux-arm64": "1.3.0", + "@img/sharp-libvips-linux-ppc64": "1.3.0", + "@img/sharp-libvips-linux-riscv64": "1.3.0", + "@img/sharp-libvips-linux-s390x": "1.3.0", + "@img/sharp-libvips-linux-x64": "1.3.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", + "@img/sharp-libvips-linuxmusl-x64": "1.3.0", + "@img/sharp-linux-arm": "0.35.1", + "@img/sharp-linux-arm64": "0.35.1", + "@img/sharp-linux-ppc64": "0.35.1", + "@img/sharp-linux-riscv64": "0.35.1", + "@img/sharp-linux-s390x": "0.35.1", + "@img/sharp-linux-x64": "0.35.1", + "@img/sharp-linuxmusl-arm64": "0.35.1", + "@img/sharp-linuxmusl-x64": "0.35.1", + "@img/sharp-webcontainers-wasm32": "0.35.1", + "@img/sharp-win32-arm64": "0.35.1", + "@img/sharp-win32-ia32": "0.35.1", + "@img/sharp-win32-x64": "0.35.1" + } + }, + "node_modules/shiki": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz", + "integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.2.0", + "@shikijs/engine-javascript": "4.2.0", + "@shikijs/engine-oniguruma": "4.2.0", + "@shikijs/langs": "4.2.0", + "@shikijs/themes": "4.2.0", + "@shikijs/types": "4.2.0", + "@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/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/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.14", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.14.tgz", + "integrity": "sha512-F1oWdz8tjT17qe1d5JgDK6z03WGOhYYAN0lK3/D/fzNiy93xswLLEw7pk+3g05onhAy6Bsc6PLNUGhdgVjemMQ==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/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/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.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "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.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "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.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "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/site/package.json b/site/package.json new file mode 100644 index 00000000..fc2fd7ec --- /dev/null +++ b/site/package.json @@ -0,0 +1,16 @@ +{ + "name": "echo-docs", + "type": "module", + "version": "0.1.0", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/starlight": "0.40.0", + "astro": "6.4.6", + "sharp": "0.35.1" + } +} diff --git a/site/public/CNAME b/site/public/CNAME new file mode 100644 index 00000000..9f703cd8 --- /dev/null +++ b/site/public/CNAME @@ -0,0 +1 @@ +echo.labstack.com diff --git a/site/public/apple-touch-icon.png b/site/public/apple-touch-icon.png new file mode 100644 index 00000000..383f999b Binary files /dev/null and b/site/public/apple-touch-icon.png differ diff --git a/site/public/favicon.ico b/site/public/favicon.ico new file mode 100644 index 00000000..74f4470a Binary files /dev/null and b/site/public/favicon.ico differ diff --git a/site/public/favicon.svg b/site/public/favicon.svg new file mode 100644 index 00000000..b05b21d6 --- /dev/null +++ b/site/public/favicon.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> + <rect width="64" height="64" rx="15" fill="#0d0b0b"/> + <polygon points="16,16 30,24 30,40 16,32" fill="#00afd1"/> + <polygon points="32,24 46,32 46,48 32,40" fill="#4ae1ff"/> + <polygon points="18,34 32,42 32,52 18,44" fill="#00afd1" opacity="0.55"/> +</svg> diff --git a/site/public/og.png b/site/public/og.png new file mode 100644 index 00000000..891bcdca Binary files /dev/null and b/site/public/og.png differ diff --git a/site/src/assets/logo-dark.svg b/site/src/assets/logo-dark.svg new file mode 100644 index 00000000..f1c09baa --- /dev/null +++ b/site/src/assets/logo-dark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111 36.7"><g transform="translate(-16,-15.7)"><polygon points="16,16 30,24 30,40 16,32" fill="#00AFD1"/><polygon points="32,24 46,32 46,48 32,40" fill="#4AE1FF"/><polygon points="18,34 32,42 32,52 18,44" fill="#00AFD1" opacity="0.55"/></g><g transform="translate(-8,0)" fill="#ffffff"><path d="M52.5,22.3c0.3,2,2,3.4,4.8,3.4c1.5,0,3.4-0.5,4.3-1.5l2.5,2.5c-1.7,1.7-4.4,2.6-6.9,2.6c-5.5,0-8.8-3.4-8.8-8.4c0-4.8,3.3-8.3,8.5-8.3c5.3,0,8.7,3.3,8.1,9.8H52.5z M61.2,19.1c-0.3-2-1.9-3.1-4.1-3.1c-2.1,0-3.9,1-4.5,3.1H61.2z"/><path d="M81.7,26.8c-1.8,1.8-3.8,2.5-6.1,2.5c-4.6,0-8.5-2.8-8.5-8.4c0-5.6,3.9-8.4,8.5-8.4c2.3,0,4,0.6,5.8,2.3l-2.5,2.6c-0.9-0.8-2.1-1.2-3.2-1.2C73,16.3,71,18.2,71,21c0,3,2.1,4.6,4.5,4.6c1.3,0,2.5-0.4,3.5-1.3L81.7,26.8z"/><path d="M87.4,6.4v8.8c1.4-1.8,3.2-2.4,5-2.4c4.5,0,6.5,3,6.5,7.7v8.3H95v-8.3c0-2.9-1.5-4.1-3.6-4.1c-2.3,0-3.9,2-3.9,4.3v8.1h-3.9V6.4H87.4z"/><path d="M118.1,21c0,4.5-3.1,8.2-8.3,8.2c-5.2,0-8.3-3.7-8.3-8.2c0-4.5,3.2-8.2,8.3-8.2S118.1,16.5,118.1,21z M105.5,21c0,2.4,1.5,4.6,4.3,4.6c2.9,0,4.3-2.2,4.3-4.6c0-2.4-1.7-4.7-4.3-4.7C107,16.3,105.5,18.6,105.5,21z"/></g></svg> diff --git a/site/src/assets/logo-light.svg b/site/src/assets/logo-light.svg new file mode 100644 index 00000000..429e0363 --- /dev/null +++ b/site/src/assets/logo-light.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111 36.7"><g transform="translate(-16,-15.7)"><polygon points="16,16 30,24 30,40 16,32" fill="#00AFD1"/><polygon points="32,24 46,32 46,48 32,40" fill="#4AE1FF"/><polygon points="18,34 32,42 32,52 18,44" fill="#00AFD1" opacity="0.55"/></g><g transform="translate(-8,0)" fill="#292E31"><path d="M52.5,22.3c0.3,2,2,3.4,4.8,3.4c1.5,0,3.4-0.5,4.3-1.5l2.5,2.5c-1.7,1.7-4.4,2.6-6.9,2.6c-5.5,0-8.8-3.4-8.8-8.4c0-4.8,3.3-8.3,8.5-8.3c5.3,0,8.7,3.3,8.1,9.8H52.5z M61.2,19.1c-0.3-2-1.9-3.1-4.1-3.1c-2.1,0-3.9,1-4.5,3.1H61.2z"/><path d="M81.7,26.8c-1.8,1.8-3.8,2.5-6.1,2.5c-4.6,0-8.5-2.8-8.5-8.4c0-5.6,3.9-8.4,8.5-8.4c2.3,0,4,0.6,5.8,2.3l-2.5,2.6c-0.9-0.8-2.1-1.2-3.2-1.2C73,16.3,71,18.2,71,21c0,3,2.1,4.6,4.5,4.6c1.3,0,2.5-0.4,3.5-1.3L81.7,26.8z"/><path d="M87.4,6.4v8.8c1.4-1.8,3.2-2.4,5-2.4c4.5,0,6.5,3,6.5,7.7v8.3H95v-8.3c0-2.9-1.5-4.1-3.6-4.1c-2.3,0-3.9,2-3.9,4.3v8.1h-3.9V6.4H87.4z"/><path d="M118.1,21c0,4.5-3.1,8.2-8.3,8.2c-5.2,0-8.3-3.7-8.3-8.2c0-4.5,3.2-8.2,8.3-8.2S118.1,16.5,118.1,21z M105.5,21c0,2.4,1.5,4.6,4.3,4.6c2.9,0,4.3-2.2,4.3-4.6c0-2.4-1.7-4.7-4.3-4.7C107,16.3,105.5,18.6,105.5,21z"/></g></svg> diff --git a/site/src/components/AskEcho.astro b/site/src/components/AskEcho.astro new file mode 100644 index 00000000..5e525894 --- /dev/null +++ b/site/src/components/AskEcho.astro @@ -0,0 +1,110 @@ +--- +// Vanilla Astro island — no React. Fab + ⌘-style modal with a (stubbed) streamed answer. +// NOTE: the answer is a hardcoded DEMO, not a live model. To connect a real provider, +// replace the timer block in ask() with a fetch (see website/src/components/AskEcho.js +// for a kapa.ai-style sketch). +const SUGGESTIONS = [ + { icon: 'ph-lock-key', q: 'How do I add JWT authentication?' }, + { icon: 'ph-globe', q: 'How do I enable CORS?' }, + { icon: 'ph-link', q: 'How do I bind a JSON request body?' }, + { icon: 'ph-folder-open', q: 'How do I serve static files?' }, +]; +--- +<button class="ask-fab" id="ask-fab" type="button" aria-label="Ask Echo"> + <i class="ph ph-sparkle"></i> Ask Echo <kbd>AI</kbd> +</button> + +<div class="ask-overlay" id="ask-overlay" hidden> + <div class="ask-palette" role="dialog" aria-label="Ask Echo"> + <div class="ask-top"> + <i class="ph ph-sparkle"></i> + <input class="ask-input" id="ask-input" placeholder="Ask Echo a question…" autocomplete="off" /> + <span class="ask-esc">ESC</span> + </div> + <div class="ask-body" id="ask-body"> + <div id="ask-suggest"> + {SUGGESTIONS.map((s) => ( + <button class="ask-s" data-q={s.q} type="button"><i class={`ph ${s.icon}`}></i> {s.q}</button> + ))} + <p class="ask-demo">Demo — answers are illustrative and not yet connected to a live model.</p> + </div> + </div> + </div> +</div> + +<style> + .ask-demo { margin: 16px 4px 2px; font-size: 0.72rem; line-height: 1.5; color: var(--sl-color-gray-4); font-family: var(--sl-font-mono); } +</style> + +<script> + function initAskEcho() { + const fab = document.getElementById('ask-fab'); + const overlay = document.getElementById('ask-overlay'); + const input = document.getElementById('ask-input') as HTMLInputElement | null; + const body = document.getElementById('ask-body'); + if (!fab || !overlay || !input || !body) { + console.error('[AskEcho] missing required DOM nodes; island not initialized'); + return; + } + // Portal to <body> so position:fixed + z-index escape any Starlight stacking context. + document.body.appendChild(fab); + document.body.appendChild(overlay); + const suggestHTML = body.innerHTML; + let timer: ReturnType<typeof setInterval> | undefined; + let pending: ReturnType<typeof setTimeout> | undefined; + + // Hardcoded DEMO answer — swap the timer block in ask() for a real provider fetch. + const ANSWER = + `To add JWT authentication, use Echo's built-in <strong>JWT middleware</strong>. Register it on the routes (or group) you want to protect:\n\n<pre>import echojwt "github.com/labstack/echo-jwt/v5"\n\ne.Use(echojwt.WithConfig(echojwt.Config{\n SigningKey: []byte("your-secret"),\n}))</pre>\n\nRequests must then send <code>Authorization: Bearer <token></code>. Read claims with <code>c.Get("user")</code>.`; + const SOURCES = ['Guide › Middleware › JWT', 'Cookbook › JWT Authentication', 'API › echo-jwt']; + + function bindSuggest() { + body.querySelectorAll<HTMLButtonElement>('.ask-s').forEach((b) => + b.addEventListener('click', () => ask(b.dataset.q || '')) + ); + } + function open() { + overlay.hidden = false; fab.hidden = true; + body.innerHTML = suggestHTML; bindSuggest(); input.value = ''; + setTimeout(() => input.focus(), 40); + } + function close() { + overlay.hidden = true; fab.hidden = false; + if (timer) clearInterval(timer); + if (pending) clearTimeout(pending); + } + function sourcesHTML() { + return '<div class="ask-sources"><h5>Sources</h5>' + + SOURCES.map((s, i) => `<div class="ask-src"><span class="ask-n">${i + 1}</span> ${s}</div>`).join('') + + '</div>'; + } + function ask(q: string) { + input.value = q; + body.innerHTML = '<div class="ask-badge"><span class="ask-dot"></span> Ask Echo is thinking…</div><div class="ask-answer" id="ask-ans"></div>'; + const ans = document.getElementById('ask-ans'); + if (!ans) return; + pending = setTimeout(() => { + const badge = ans.parentElement?.querySelector('.ask-badge'); + if (badge) badge.innerHTML = '<span class="ask-dot"></span> Ask Echo'; + let i = 0; + timer = setInterval(() => { + i += 5; + ans.innerHTML = ANSWER.slice(0, i).replace(/\n/g, '<br/>') + '<span class="ask-cursor"></span>'; + if (i >= ANSWER.length) { + if (timer) clearInterval(timer); + ans.innerHTML = ANSWER.replace(/\n/g, '<br/>'); + ans.insertAdjacentHTML('beforeend', sourcesHTML()); + } + }, 12); + }, 420); + } + + bindSuggest(); + fab.addEventListener('click', open); + window.addEventListener('ask-echo-open', open); + overlay.addEventListener('mousedown', (e) => { if (e.target === overlay) close(); }); + input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && input.value.trim()) ask(input.value.trim()); }); + window.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); + } + initAskEcho(); +</script> diff --git a/site/src/components/DocActions.astro b/site/src/components/DocActions.astro new file mode 100644 index 00000000..44c45146 --- /dev/null +++ b/site/src/components/DocActions.astro @@ -0,0 +1,8 @@ +<div class="doc-actions"> + <button class="doc-act doc-act--primary" data-ask-echo type="button"><i class="ph ph-sparkle"></i> Ask Echo</button> +</div> +<script> + document.querySelectorAll('[data-ask-echo]').forEach((b) => + b.addEventListener('click', () => window.dispatchEvent(new Event('ask-echo-open'))) + ); +</script> diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro new file mode 100644 index 00000000..fe49f659 --- /dev/null +++ b/site/src/components/Footer.astro @@ -0,0 +1,19 @@ +--- +import Default from '@astrojs/starlight/components/Footer.astro'; +import AskEcho from './AskEcho.astro'; + +const year = new Date().getFullYear(); +--- +<Default><slot /></Default> +<AskEcho /> +<footer class="echo-docfoot"> + <span>© {year} LabStack LLC. Released under the MIT License.</span> + <div class="echo-social"> + <a href="https://x.com/labstack" target="_blank" rel="noopener noreferrer" aria-label="LabStack on X"> + <i class="ph ph-x-logo"></i> + </a> + <a href="https://github.com/labstack/echo" target="_blank" rel="noopener noreferrer" aria-label="Echo on GitHub"> + <i class="ph ph-github-logo"></i> + </a> + </div> +</footer> diff --git a/site/src/components/HomeHero.astro b/site/src/components/HomeHero.astro new file mode 100644 index 00000000..4d5dad99 --- /dev/null +++ b/site/src/components/HomeHero.astro @@ -0,0 +1,109 @@ +--- +// Living Terminal hero: code window + a terminal pane that types `curl` +// and streams Echo's JSON response. Animation is client-side, with a +// static final-state fallback for reduced-motion / no-JS. +import { starsLabel } from '../data/github.ts'; +--- + +<section class="hh"> + <div class="hh-left"> + <span class="hh-eyebrow"><i class="ph ph-sparkle"></i> Echo v5.2 — now released</span> + <h1>Build fast Go APIs.<br /><span class="g">Without the bloat.</span></h1> + <p class="hh-sub"> + A high-performance, minimalist Go web framework — a zero-allocation + router, batteries-included middleware, and an expressive API. Ship + production services in minutes. + </p> + <div class="hh-cta"> + <a class="hh-btn pri" href="/guide/quickstart/">Get Started <i class="ph ph-arrow-right"></i></a> + <a class="hh-btn sec" href="https://github.com/labstack/echo" target="_blank" rel="noopener noreferrer"><i class="ph ph-star"></i> {starsLabel} on GitHub</a> + </div> + <button class="hh-install" type="button" data-copy="go get github.com/labstack/echo/v5" aria-label="Copy install command"> + <span class="p">$</span> go get github.com/labstack/echo/v5 <i class="ph ph-copy cp"></i> + </button> + </div> + + <div class="hh-win" aria-hidden="true"> + <div class="bar"> + <i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i> + <span class="fn">main.go</span> + </div> + <pre><span class="kw">package</span> main + +<span class="kw">import</span> <span class="str">"github.com/labstack/echo/v5"</span> + +<span class="kw">func</span> <span class="fnn">main</span>() { + e := echo.<span class="fnn">New</span>() + e.<span class="fnn">GET</span>(<span class="str">"/"</span>, <span class="kw">func</span>(c *echo.Context) <span class="kw">error</span> { + <span class="kw">return</span> c.<span class="fnn">JSON</span>(<span class="str">200</span>, echo.Map{<span class="str">"message"</span>: <span class="str">"Hello, World!"</span>}) + }) + e.<span class="fnn">Start</span>(<span class="str">":1323"</span>) +}</pre> + <div class="hh-term" data-term> + <div><span class="pr">~/echo $</span> <span data-cmd></span><span class="hh-cur" data-cur></span></div> + <div data-out></div> + </div> + </div> +</section> + +<script> + // --- install copy button --- + document.querySelectorAll<HTMLButtonElement>('.hh-install').forEach((btn) => { + btn.addEventListener('click', async () => { + const text = btn.dataset.copy || ''; + try { + await navigator.clipboard.writeText(text); + const icon = btn.querySelector('.cp'); + if (icon) { + icon.classList.remove('ph-copy'); + icon.classList.add('ph-check'); + setTimeout(() => { + icon.classList.remove('ph-check'); + icon.classList.add('ph-copy'); + }, 1400); + } + } catch {} + }); + }); + + // --- terminal typing animation --- + const term = document.querySelector('[data-term]'); + if (term) { + const cmdEl = term.querySelector('[data-cmd]') as HTMLElement; + const outEl = term.querySelector('[data-out]') as HTMLElement; + const curEl = term.querySelector('[data-cur]') as HTMLElement; + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + const response = + '<span class="dim">HTTP/1.1 200 OK · 0 allocations</span>\n' + + '<span class="json">{ "message": "Hello, World!" }</span>'; + const cmd = 'curl localhost:1323'; + + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + + async function run() { + while (true) { + cmdEl.textContent = ''; + outEl.innerHTML = ''; + curEl.style.display = 'inline-block'; + await sleep(900); + for (const ch of cmd) { + cmdEl.textContent += ch; + await sleep(55); + } + await sleep(450); + curEl.style.display = 'none'; + outEl.innerHTML = '<pre style="margin:6px 0 0;padding:0;background:none;font:inherit">' + response + '</pre>'; + await sleep(3600); + } + } + + if (reduce) { + cmdEl.textContent = cmd; + curEl.style.display = 'none'; + outEl.innerHTML = '<pre style="margin:6px 0 0;padding:0;background:none;font:inherit">' + response + '</pre>'; + } else { + run(); + } + } +</script> diff --git a/site/src/components/PageTitle.astro b/site/src/components/PageTitle.astro new file mode 100644 index 00000000..6a3266df --- /dev/null +++ b/site/src/components/PageTitle.astro @@ -0,0 +1,6 @@ +--- +import Default from '@astrojs/starlight/components/PageTitle.astro'; +import DocActions from './DocActions.astro'; +--- +<Default><slot /></Default> +<DocActions /> diff --git a/site/src/content.config.ts b/site/src/content.config.ts new file mode 100644 index 00000000..c30483bf --- /dev/null +++ b/site/src/content.config.ts @@ -0,0 +1,13 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; +import { z } from 'astro:schema'; + +export const collections = { + docs: defineCollection({ + loader: docsLoader(), + // `description` is required (not optional as in the stock schema): every page uses it + // for SEO/OG meta, so enforce it at build time rather than by convention. + schema: docsSchema({ extend: z.object({ description: z.string().min(1) }) }), + }), +}; diff --git a/site/src/content/docs/cookbook/auto-tls.md b/site/src/content/docs/cookbook/auto-tls.md new file mode 100644 index 00000000..f6d385d7 --- /dev/null +++ b/site/src/content/docs/cookbook/auto-tls.md @@ -0,0 +1,110 @@ +--- +title: Auto TLS +description: Automatically obtain and renew TLS certificates from Let's Encrypt. +sidebar: + order: 3 +--- + +This recipe obtains TLS certificates for a domain automatically from Let's Encrypt. +Configure a `StartConfig` with the autocert manager's `TLSConfig` and listen on +port `443`. + +Browse to `https://<DOMAIN>`. If everything is configured correctly, you should see +a welcome message served over TLS. + +:::tip +- For added security, specify a host policy in the autocert manager. +- Cache certificates to avoid hitting [Let's Encrypt rate limits](https://letsencrypt.org/docs/rate-limits). +- To redirect HTTP traffic to HTTPS, use the [redirect middleware](/middleware/redirect/#https-redirect). +::: + +## Server + +```go +package main + +import ( + "context" + "crypto/tls" + "errors" + "log/slog" + "net/http" + "os" + + "golang.org/x/crypto/acme" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + e := echo.New() + e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + e.Use(middleware.Recover()) + e.Use(middleware.RequestLogger()) + + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, ` + <h1>Welcome to Echo!</h1> + <h3>TLS certificates automatically installed from Let's Encrypt :)</h3> + `) + }) + + m := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist("example.com", "www.example.com"), + // Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) + Cache: autocert.DirCache("/var/www/.cache"), + // Email: "[email protected]", // optional but recommended + } + + sc := echo.StartConfig{ + Address: ":443", + TLSConfig: m.TLSConfig(), + } + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Using a custom HTTP server + +If you need full control over the `http.Server`, wire the autocert manager into a +custom `tls.Config` instead: + +```go +func customHTTPServer() { + e := echo.New() + e.Use(middleware.Recover()) + e.Use(middleware.RequestLogger()) + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, ` + <h1>Welcome to Echo!</h1> + <h3>TLS certificates automatically installed from Let's Encrypt :)</h3> + `) + }) + + autoTLSManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + // Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) + Cache: autocert.DirCache("/var/www/.cache"), + //HostPolicy: autocert.HostWhitelist("<DOMAIN>"), + } + s := http.Server{ + Addr: ":443", + Handler: e, // set Echo as handler + TLSConfig: &tls.Config{ + //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field + GetCertificate: autoTLSManager.GetCertificate, + NextProtos: []string{acme.ALPNProto}, + }, + //ReadTimeout: 30 * time.Second, // use custom timeouts + } + if err := s.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/cors.md b/site/src/content/docs/cookbook/cors.md new file mode 100644 index 00000000..1fca9e36 --- /dev/null +++ b/site/src/content/docs/cookbook/cors.md @@ -0,0 +1,120 @@ +--- +title: CORS +description: Enable Cross-Origin Resource Sharing with an allow list or a custom origin function. +sidebar: + order: 4 +--- + +The [CORS middleware](/middleware/cors/) controls which origins may access your API. +You can pass a fixed list of allowed origins, or supply a function that decides +per request. + +## Allow list of origins + +Pass the allowed origins directly to `middleware.CORS`. + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var ( + users = []string{"Joe", "Veer", "Zion"} +) + +func getUsers(c *echo.Context) error { + return c.JSON(http.StatusOK, users) +} + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // CORS default + // Allows requests from any origin wth GET, HEAD, PUT, POST or DELETE method. + // e.Use(middleware.CORS("*")) + + // CORS restricted + // Allows requests from any `https://labstack.com` or `https://labstack.net` origin + e.Use(middleware.CORS("https://labstack.com", "https://labstack.net")) + + e.GET("/api/users", getUsers) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Custom origin function + +For dynamic policies, use `CORSWithConfig` with `UnsafeAllowOriginFunc`. The +function receives the request context and origin and returns the origin to echo +back, whether the request is allowed, and an optional error. + +```go +package main + +import ( + "context" + "net/http" + "strings" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var ( + users = []string{"Joe", "Veer", "Zion"} +) + +func getUsers(c *echo.Context) error { + return c.JSON(http.StatusOK, users) +} + +// allowOrigin takes the origin as an argument and returns: +// - origin to add to the response Access-Control-Allow-Origin header +// - whether the request is allowed or not +// - an optional error. this will stop handler chain execution and return an error response. +// +// return origin, true, err // blocks request with error +// return origin, true, nil // allows CSRF request through +// return origin, false, nil // falls back to legacy token logic +func allowOrigin(c *echo.Context, origin string) (string, bool, error) { + // In this example we use a naive suffix check but we can imagine various + // kind of custom logic. For example, an external datasource could be used + // to maintain the list of allowed origins. + if strings.HasSuffix(origin, ".example.com") { + return origin, true, nil + } + return "", false, nil +} + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // CORS restricted with a custom function to allow origins + // and with the GET, PUT, POST or DELETE methods allowed. + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + UnsafeAllowOriginFunc: allowOrigin, + AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, + })) + + e.GET("/api/users", getUsers) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/crud.md b/site/src/content/docs/cookbook/crud.md new file mode 100644 index 00000000..e3218630 --- /dev/null +++ b/site/src/content/docs/cookbook/crud.md @@ -0,0 +1,180 @@ +--- +title: CRUD +description: Create, read, update, and delete resources with Echo and JSON binding. +sidebar: + order: 2 +--- + +A complete CRUD (create, read, update, delete) API backed by an in-memory store. +Each handler binds the JSON request body into a struct, mutates the store under a +lock, and returns the result as JSON. + +## Server + +```go +package main + +import ( + "context" + "net/http" + "strconv" + "sync" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +type ( + user struct { + ID int `json:"id"` + Name string `json:"name"` + } +) + +var ( + users = map[int]*user{} + seq = 1 + lock = sync.Mutex{} +) + +//---------- +// Handlers +//---------- + +func createUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + u := &user{ + ID: seq, + } + if err := c.Bind(u); err != nil { + return err + } + users[u.ID] = u + seq++ + return c.JSON(http.StatusCreated, u) +} + +func getUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + id, _ := strconv.Atoi(c.Param("id")) + return c.JSON(http.StatusOK, users[id]) +} + +func updateUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + u := new(user) + if err := c.Bind(u); err != nil { + return err + } + id, _ := strconv.Atoi(c.Param("id")) + users[id].Name = u.Name + return c.JSON(http.StatusOK, users[id]) +} + +func deleteUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + id, _ := strconv.Atoi(c.Param("id")) + delete(users, id) + return c.NoContent(http.StatusNoContent) +} + +func getAllUsers(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + return c.JSON(http.StatusOK, users) +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Routes + e.GET("/users", getAllUsers) + e.POST("/users", createUser) + e.GET("/users/:id", getUser) + e.PUT("/users/:id", updateUser) + e.DELETE("/users/:id", deleteUser) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Client + +### Create user + +Request: + +```sh +curl -X POST \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe Smith"}' \ + localhost:1323/users +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### Get user + +Request: + +```sh +curl localhost:1323/users/1 +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### Update user + +Request: + +```sh +curl -X PUT \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe"}' \ + localhost:1323/users/1 +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe" +} +``` + +### Delete user + +Request: + +```sh +curl -X DELETE localhost:1323/users/1 +``` + +Response: `204 No Content`. diff --git a/site/src/content/docs/cookbook/embed-resources.md b/site/src/content/docs/cookbook/embed-resources.md new file mode 100644 index 00000000..5a212ff5 --- /dev/null +++ b/site/src/content/docs/cookbook/embed-resources.md @@ -0,0 +1,65 @@ +--- +title: Embed Resources +description: Serve static assets bundled into the binary with Go's embed package. +sidebar: + order: 5 +--- + +Go's `embed` package (Go 1.16+) lets you compile static assets directly into the +binary, so a single executable can ship with its frontend. This recipe serves the +embedded filesystem through Echo, with an optional live mode that reads from disk +during development. + +## Server + +```go +package main + +import ( + "context" + "embed" + "io/fs" + "log" + "net/http" + "os" + + "github.com/labstack/echo/v5" +) + +//go:embed app +var embededFiles embed.FS + +func getFileSystem(useOS bool) http.FileSystem { + if useOS { + log.Print("using live mode") + return http.FS(os.DirFS("app")) + } + + log.Print("using embed mode") + fsys, err := fs.Sub(embededFiles, "app") + if err != nil { + panic(err) + } + + return http.FS(fsys) +} + +func main() { + e := echo.New() + useOS := len(os.Args) > 1 && os.Args[1] == "live" + assetHandler := http.FileServer(getFileSystem(useOS)) + e.GET("/", echo.WrapHandler(assetHandler)) + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +:::tip +Run the binary with the `live` argument (`go run server.go live`) to serve assets +from the `app` directory on disk instead of the embedded copy, which is handy +during development. +::: diff --git a/site/src/content/docs/cookbook/file-download.md b/site/src/content/docs/cookbook/file-download.md new file mode 100644 index 00000000..b538b362 --- /dev/null +++ b/site/src/content/docs/cookbook/file-download.md @@ -0,0 +1,176 @@ +--- +title: File Download +description: Serve files for download, inline display, or as named attachments. +sidebar: + order: 6 +--- + +Echo provides three context helpers for returning files: `c.File` serves a file +using the browser's default content disposition, `c.Inline` hints the browser to +display the file in place, and `c.Attachment` prompts a download with a given +filename. + +## Download file + +### Server + +```go +package main + +import ( + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.File("index.html") + }) + e.GET("/file", func(c *echo.Context) error { + return c.File("echo.svg") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Client + +```html +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>File download + + + +

+ File download +

+ + + +``` + +## Download file as inline + +Use `c.Inline` to send a `Content-Disposition: inline` header so the browser +renders the file in place rather than downloading it. + +### Server + +```go +package main + +import ( + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.File("index.html") + }) + e.GET("/inline", func(c *echo.Context) error { + return c.Inline("inline.txt", "inline.txt") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Client + +```html + + + + + File download + + + +

+ Inline file download +

+ + + +``` + +## Download file as attachment + +Use `c.Attachment` to send a `Content-Disposition: attachment` header, prompting +the browser to download the file under the supplied name. + +### Server + +```go +package main + +import ( + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.File("index.html") + }) + e.GET("/attachment", func(c *echo.Context) error { + return c.Attachment("attachment.txt", "attachment.txt") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Client + +```html + + + + + File download + + + +

+ Attachment file download +

+ + + +``` diff --git a/site/src/content/docs/cookbook/file-upload.md b/site/src/content/docs/cookbook/file-upload.md new file mode 100644 index 00000000..d6fcc978 --- /dev/null +++ b/site/src/content/docs/cookbook/file-upload.md @@ -0,0 +1,198 @@ +--- +title: File Upload +description: Handle single and multiple multipart file uploads alongside form fields. +sidebar: + order: 7 +--- + +Echo reads multipart form data through the request context. Use `c.FormValue` for +text fields, `c.FormFile` for a single file, and `c.MultipartForm` to access +multiple files under the same field name. + +## Upload a single file with fields + +### Server + +```go +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func upload(c *echo.Context) error { + // Read form fields + name := c.FormValue("name") + email := c.FormValue("email") + + //----------- + // Read file + //----------- + + // Source + file, err := c.FormFile("file") + if err != nil { + return err + } + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(file.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + return c.HTML(http.StatusOK, fmt.Sprintf("

File %s uploaded successfully with fields name=%s and email=%s.

", file.Filename, name, email)) +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "public") + e.POST("/upload", upload) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Client + +```html + + + + + Single file upload + + +

Upload single file with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + +``` + +## Upload multiple files with fields + +### Server + +```go +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func upload(c *echo.Context) error { + // Read form fields + name := c.FormValue("name") + email := c.FormValue("email") + + //------------ + // Read files + //------------ + + // Multipart form + form, err := c.MultipartForm() + if err != nil { + return err + } + files := form.File["files"] + + for _, file := range files { + // Source + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(file.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + } + + return c.HTML(http.StatusOK, fmt.Sprintf("

Uploaded successfully %d files with fields name=%s and email=%s.

", len(files), name, email)) +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "public") + e.POST("/upload", upload) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Client + +```html + + + + + Multiple file upload + + +

Upload multiple files with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + +``` diff --git a/site/src/content/docs/cookbook/graceful-shutdown.md b/site/src/content/docs/cookbook/graceful-shutdown.md new file mode 100644 index 00000000..2caf6591 --- /dev/null +++ b/site/src/content/docs/cookbook/graceful-shutdown.md @@ -0,0 +1,86 @@ +--- +title: Graceful Shutdown +description: Drain in-flight requests before stopping the server on an interrupt signal. +sidebar: + order: 8 +--- + +A graceful shutdown lets in-flight requests finish before the process exits. The +simplest approach is to pass a cancellable context to `StartConfig.Start` and set +a `GracefulTimeout`. When the context is cancelled by an interrupt signal, Echo +stops accepting new connections and waits up to the timeout for active requests to +complete. + +## Server + +```go +package main + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v5" +) + +func main() { + // Setup + e := echo.New() + e.GET("/", func(c *echo.Context) error { + time.Sleep(5 * time.Second) + return c.JSON(http.StatusOK, "OK") + }) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + sc := echo.StartConfig{ + Address: ":1323", + GracefulTimeout: 5 * time.Second, + } + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Using a custom HTTP server + +If you manage the `http.Server` yourself, start it in a goroutine, wait on the +signal context, then call `Shutdown` with a timeout: + +```go +func mainWithHTTPServer() { + // Setup + e := echo.New() + e.GET("/", func(c *echo.Context) error { + time.Sleep(5 * time.Second) + return c.JSON(http.StatusOK, "OK") + }) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + s := http.Server{Addr: ":1323", Handler: e} + // Start server + go func() { + if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + e.Logger.Error("failed to start server", "error", err) + } + }() + + // Wait for interrupt signal to gracefully shut down the server with a timeout of 10 seconds. + <-ctx.Done() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := s.Shutdown(ctx); err != nil { + e.Logger.Error("failed to stop server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/hello-world.md b/site/src/content/docs/cookbook/hello-world.md new file mode 100644 index 00000000..e41fb41d --- /dev/null +++ b/site/src/content/docs/cookbook/hello-world.md @@ -0,0 +1,43 @@ +--- +title: Hello World +description: A minimal Echo server that responds with a greeting. +sidebar: + order: 1 +--- + +A minimal Echo application: create an instance, register the Logger and Recover +middleware, add a single route, and start the server. + +## Server + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Route => handler + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!\n") + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/http2-server-push.md b/site/src/content/docs/cookbook/http2-server-push.md new file mode 100644 index 00000000..c11004c8 --- /dev/null +++ b/site/src/content/docs/cookbook/http2-server-push.md @@ -0,0 +1,130 @@ +--- +title: HTTP/2 Server Push +description: Push web assets to the client proactively over HTTP/2. +sidebar: + order: 10 +--- + +HTTP/2 server push lets the server send resources to the client before they are +requested, eliminating a round trip for assets the page is known to need. This +recipe pushes a page's CSS, JavaScript, and image alongside the HTML response. + +:::note +Server push requires an HTTP/2 connection. Follow [Generate a self-signed X.509 +TLS certificate](/cookbook/http2/#1-generate-a-self-signed-x509-tls-certificate) +to create the certificate used below. +::: + +## 1. Register a route to serve web assets + +```go +e.Static("/", "static") +``` + +## 2. Serve index.html and push its dependencies + +Unwrap the response to access the underlying `http.ResponseWriter`, then push each +asset if the writer implements `http.Pusher`: + +```go +e.GET("/", func(c *echo.Context) (err error) { + rw, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return + } + if pusher, ok := rw.ResponseWriter.(http.Pusher); ok { + if err = pusher.Push("/app.css", nil); err != nil { + return + } + if err = pusher.Push("/app.js", nil); err != nil { + return + } + if err = pusher.Push("/echo.png", nil); err != nil { + return + } + } + return c.File("index.html") +}) +``` + +:::tip +When `http.Pusher` is supported, the web assets are pushed proactively; otherwise +the client falls back to requesting them separately. +::: + +## 3. Start the TLS server + +```go +sc := echo.StartConfig{Address: ":1323"} +if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) +} +``` + +## Source code + +### index.html + +```html + + + + + + + HTTP/2 Server Push + + + + + +

The following static files are served via HTTP/2 server push

+ + + +``` + +### server.go + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() + e.Static("/", "static") + e.GET("/", func(c *echo.Context) (err error) { + rw, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return + } + if pusher, ok := rw.ResponseWriter.(http.Pusher); ok { + if err = pusher.Push("/app.css", nil); err != nil { + return + } + if err = pusher.Push("/app.js", nil); err != nil { + return + } + if err = pusher.Push("/echo.png", nil); err != nil { + return + } + } + return c.File("index.html") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/http2.md b/site/src/content/docs/cookbook/http2.md new file mode 100644 index 00000000..1eeef540 --- /dev/null +++ b/site/src/content/docs/cookbook/http2.md @@ -0,0 +1,116 @@ +--- +title: HTTP/2 Server +description: Serve traffic over HTTP/2 by starting Echo with a TLS certificate. +sidebar: + order: 9 +--- + +HTTP/2 improves latency through request multiplexing, header compression, and +server push. Go's HTTP server negotiates HTTP/2 automatically over TLS, so serving +HTTP/2 with Echo is a matter of starting the server with a certificate. + +## 1. Generate a self-signed X.509 TLS certificate + +Run the following command to generate `cert.pem` and `key.pem`: + +```sh +go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost +``` + +:::note +For demonstration purposes we use a self-signed certificate. In production, obtain +a certificate from a [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority). +::: + +## 2. Create a handler that echoes request information + +```go +e.GET("/request", func(c *echo.Context) error { + req := c.Request() + format := ` + + Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+ ` + return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) +}) +``` + +## 3. Start the TLS server + +Start the server with the generated certificate and key: + +```go +sc := echo.StartConfig{Address: ":1323"} +if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) +} +``` + +Alternatively, use a custom `http.Server` with your own `tls.Config`: + +```go +s := http.Server{ + Addr: ":8443", + Handler: e, // set Echo as handler + TLSConfig: &tls.Config{ + //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field + }, + //ReadTimeout: 30 * time.Second, // use custom timeouts +} +if err := s.ListenAndServeTLS("cert.pem", "key.pem"); err != http.ErrServerClosed { + log.Fatal(err) +} +``` + +## 4. Verify + +Start the server and browse to `https://localhost:1323/request`. You should see +output similar to: + +```sh +Protocol: HTTP/2.0 +Host: localhost:1323 +Remote Address: [::1]:60288 +Method: GET +Path: / +``` + +## Source code + +```go +package main + +import ( + "context" + "fmt" + "net/http" + + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() + e.GET("/request", func(c *echo.Context) error { + req := c.Request() + format := ` + + Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+ ` + return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) + }) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/jsonp.md b/site/src/content/docs/cookbook/jsonp.md new file mode 100644 index 00000000..1b8f4857 --- /dev/null +++ b/site/src/content/docs/cookbook/jsonp.md @@ -0,0 +1,95 @@ +--- +title: JSONP +description: Serve JSONP responses for cross-domain requests with Context#JSONP. +sidebar: + order: 13 +--- + +JSONP is a technique that allows cross-domain server calls from the browser. Echo +serves JSONP responses with `c.JSONP()`, which wraps the JSON payload in a call to +the callback function named in the request. + +## Server + +```go +package main + +import ( + "context" + "math/rand" + "net/http" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "public") + + // JSONP + e.GET("/jsonp", func(c *echo.Context) error { + callback := c.QueryParam("callback") + var content struct { + Response string `json:"response"` + Timestamp time.Time `json:"timestamp"` + Random int `json:"random"` + } + content.Response = "Sent via JSONP" + content.Timestamp = time.Now().UTC() + content.Random = rand.Intn(1000) + return c.JSONP(http.StatusOK, callback, &content) + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Client + +```html + + + + + + + JSONP + + + + + + +
+ +

+


+        

+
+ + + +``` diff --git a/site/src/content/docs/cookbook/jwt.md b/site/src/content/docs/cookbook/jwt.md new file mode 100644 index 00000000..c4f126f1 --- /dev/null +++ b/site/src/content/docs/cookbook/jwt.md @@ -0,0 +1,251 @@ +--- +title: JWT +description: Authenticate requests with JSON Web Tokens using the echo-jwt middleware. +sidebar: + order: 11 +--- + +This recipe demonstrates JWT authentication with Echo using the +[`echo-jwt`](https://github.com/labstack/echo-jwt) middleware: + +- JWT authentication using the HS256 algorithm. +- The token is read from the `Authorization` request header. + +See the [JWT middleware](/middleware/jwt/) page for full configuration options. + +## Server + +### Using custom claims + +Define a claims type that embeds `jwt.RegisteredClaims`, then point the middleware +at it with `NewClaimsFunc`. Inside the restricted handler, retrieve the parsed token +from the context with the generic `echo.ContextGet`. + +```go +package main + +import ( + "context" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +// jwtCustomClaims are custom claims extending default ones. +// See https://github.com/golang-jwt/jwt for more examples +type jwtCustomClaims struct { + Name string `json:"name"` + Admin bool `json:"admin"` + jwt.RegisteredClaims +} + +func login(c *echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + // Throws unauthorized error + if username != "jon" || password != "shhh!" { + return echo.ErrUnauthorized + } + + // Set custom claims + claims := &jwtCustomClaims{ + "Jon Snow", + true, + jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)), + }, + } + + // Create token with claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]string{ + "token": t, + }) +} + +func accessible(c *echo.Context) error { + return c.String(http.StatusOK, "Accessible") +} + +func restricted(c *echo.Context) error { + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + claims := token.Claims.(*jwtCustomClaims) + name := claims.Name + return c.String(http.StatusOK, "Welcome "+name+"!") +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Login route + e.POST("/login", login) + + // Unauthenticated route + e.GET("/", accessible) + + // Restricted group + r := e.Group("/restricted") + + // Configure middleware with the custom claims type + config := echojwt.Config{ + NewClaimsFunc: func(c *echo.Context) jwt.Claims { + return new(jwtCustomClaims) + }, + SigningKey: []byte("secret"), + } + r.Use(echojwt.WithConfig(config)) + r.GET("", restricted) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Using a user-defined KeyFunc + +When tokens are signed by an external identity provider, supply a `KeyFunc` that +resolves the signing key dynamically. This example validates tokens issued by +Google Sign-In by fetching Google's public key set. + +```go +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + + echojwt "github.com/labstack/echo-jwt/v5" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/lestrrat-go/jwx/v3/jwk" +) + +func getKey(token *jwt.Token) (any, error) { + + // For a demonstration purpose, Google Sign-in is used. + // https://developers.google.com/identity/sign-in/web/backend-auth + // + // This user-defined KeyFunc verifies tokens issued by Google Sign-In. + // + // Note: In this example, it downloads the keyset every time the restricted route is accessed. + keySet, err := jwk.Fetch(context.Background(), "https://www.googleapis.com/oauth2/v3/certs") + if err != nil { + return nil, err + } + + keyID, ok := token.Header["kid"].(string) + if !ok { + return nil, errors.New("expecting JWT header to have a key ID in the kid field") + } + + key, found := keySet.LookupKeyID(keyID) + + if !found { + return nil, fmt.Errorf("unable to find key %q", keyID) + } + + return key.PublicKey() +} + +func accessible(c *echo.Context) error { + return c.String(http.StatusOK, "Accessible") +} + +func restricted(c *echo.Context) error { + user, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + claims := user.Claims.(jwt.MapClaims) + name := claims["name"].(string) + return c.String(http.StatusOK, "Welcome "+name+"!") +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Unauthenticated route + e.GET("/", accessible) + + // Restricted group + r := e.Group("/restricted") + { + config := echojwt.Config{ + KeyFunc: getKey, + } + r.Use(echojwt.WithConfig(config)) + r.GET("", restricted) + } + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +:::caution +Fetching the key set on every request, as shown above, is for demonstration only. +In production, cache the key set and refresh it periodically. +::: + +## Client + +### Login + +Log in with a username and password to retrieve a token. + +```sh +curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login +``` + +Response: + +```js +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +} +``` + +### Request + +Request a restricted resource using the token in the `Authorization` request header. + +```sh +curl localhost:1323/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +``` + +Response: + +```sh +Welcome Jon Snow! +``` diff --git a/site/src/content/docs/cookbook/load-balancing.md b/site/src/content/docs/cookbook/load-balancing.md new file mode 100644 index 00000000..74be0c50 --- /dev/null +++ b/site/src/content/docs/cookbook/load-balancing.md @@ -0,0 +1,117 @@ +--- +title: Load Balancing +description: Use Nginx as a reverse proxy to load balance traffic across multiple Echo servers. +sidebar: + order: 20 +--- + +This recipe demonstrates how to use Nginx as a reverse proxy server to load +balance traffic across multiple Echo servers. + +## Echo + +```go +package main + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var index = ` + + + + + + + Upstream Server + + + +

+ Hello from upstream server %s +

+ + +` + +func main() { + name := os.Args[1] + port := os.Args[2] + + e := echo.New() + e.Use(middleware.Recover()) + e.Use(middleware.RequestLogger()) + + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) + }) + + sc := echo.StartConfig{Address: port} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Start servers + +```sh +cd upstream +go run server.go server1 :8081 +go run server.go server2 :8082 +``` + +## Nginx + +### 1) Install Nginx + +See the [Nginx installation guide](https://www.nginx.com/resources/wiki/start/topics/tutorials/install). + +### 2) Configure Nginx + +Create a file `/etc/nginx/sites-enabled/localhost` with the following content: + +```nginx +upstream localhost { + server localhost:8081; + server localhost:8082; +} + +server { + listen 8080; + server_name localhost; + access_log /var/log/nginx/localhost.access.log combined; + + location / { + proxy_pass http://localhost; + } +} +``` + +:::note +Adjust `listen`, `server_name`, and `access_log` to suit your environment. +::: + +### 3) Restart Nginx + +```sh +service nginx restart +``` + +Browse to `https://localhost:8080`, and you should see a webpage served from +either "server 1" or "server 2". + +```sh +Hello from upstream server server1 +``` diff --git a/site/src/content/docs/cookbook/middleware.md b/site/src/content/docs/cookbook/middleware.md new file mode 100644 index 00000000..9217f341 --- /dev/null +++ b/site/src/content/docs/cookbook/middleware.md @@ -0,0 +1,140 @@ +--- +title: Custom Middleware +description: Write custom Echo middleware to collect request statistics and set response headers. +sidebar: + order: 12 +--- + +This recipe shows how to write custom middleware: + +- A middleware that collects the request count, response statuses, and uptime. +- A middleware that writes a custom `Server` header to every response. + +A middleware in Echo is a function with the signature +`func(next echo.HandlerFunc) echo.HandlerFunc`. The `Stats.Process` method below +satisfies that signature directly, while `ServerHeader` is a plain function. + +## Server + +```go +package main + +import ( + "context" + "errors" + "net/http" + "sync" + "time" + + "github.com/labstack/echo/v5" +) + +type ( + Stats struct { + Uptime time.Time `json:"uptime"` + RequestCount uint64 `json:"requestCount"` + Statuses map[int]uint64 `json:"statuses"` + mutex sync.RWMutex + } +) + +func NewStats() *Stats { + return &Stats{ + Uptime: time.Now(), + Statuses: map[int]uint64{}, + } +} + +// Process is the middleware function. +func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + err := next(c) + + status := http.StatusInternalServerError + if err != nil { + var sc echo.HTTPStatusCoder + if ok := errors.As(err, &sc); ok { + status = sc.StatusCode() + } + } else { + rw, uErr := echo.UnwrapResponse(c.Response()) + if uErr == nil { + status = rw.Status + } + err = uErr + } + + s.mutex.Lock() + defer s.mutex.Unlock() + s.RequestCount++ + s.Statuses[status]++ + + return err + } +} + +// Handle is the endpoint to get stats. +func (s *Stats) Handle(c *echo.Context) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + return c.JSON(http.StatusOK, s) +} + +// ServerHeader middleware adds a `Server` header to the response. +func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Response().Header().Set(echo.HeaderServer, "Echo/5.0") + return next(c) + } +} + +func main() { + e := echo.New() + + //------------------- + // Custom middleware + //------------------- + // Stats + s := NewStats() + e.Use(s.Process) + e.GET("/stats", s.Handle) // Endpoint to get stats + + // Server header + e.Use(ServerHeader) + + // Handler + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Response + +### Headers + +```sh +Content-Length:122 +Content-Type:application/json; charset=utf-8 +Date:Thu, 14 Apr 2016 20:31:46 GMT +Server:Echo/5.0 +``` + +### Body + +```js +{ + "uptime": "2016-04-14T13:28:48.486548936-07:00", + "requestCount": 5, + "statuses": { + "200": 4, + "404": 1 + } +} +``` diff --git a/site/src/content/docs/cookbook/reverse-proxy.md b/site/src/content/docs/cookbook/reverse-proxy.md new file mode 100644 index 00000000..d4650fa0 --- /dev/null +++ b/site/src/content/docs/cookbook/reverse-proxy.md @@ -0,0 +1,202 @@ +--- +title: Reverse Proxy +description: Use Echo as a reverse proxy and load balancer in front of upstream applications. +sidebar: + order: 19 +--- + +This recipe demonstrates how to use Echo as a reverse proxy and load balancer in +front of your applications, such as WordPress, Node.js, Java, Python, Ruby, or Go. +For simplicity, the upstreams here are Go servers that also handle WebSocket. + +## 1) Identify upstream target URL(s) + +```go +url1, err := url.Parse("http://localhost:8081") +if err != nil { + e.Logger.Error("failed parse url", "error", err) +} +url2, err := url.Parse("http://localhost:8082") +if err != nil { + e.Logger.Error("failed parse url", "error", err) +} +targets := []*middleware.ProxyTarget{ + { + URL: url1, + }, + { + URL: url2, + }, +} +``` + +## 2) Set up proxy middleware with upstream targets + +The snippet below uses round-robin load balancing. You can also use +`middleware.NewRandomBalancer()`. + +```go +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) +``` + +To set up a proxy for a sub-route, use `Echo#Group()`. + +```go +g := e.Group("/blog") +g.Use(middleware.Proxy(...)) +``` + +## 3) Start upstream servers + +```sh +cd upstream +go run server.go server1 :8081 +go run server.go server2 :8082 +``` + +## 4) Start the proxy server + +```sh +go run server.go +``` + +Browse to `http://localhost:1323`, and you should see a webpage with an HTTP +request served from "server 1" and a WebSocket request served from "server 2". + +```sh +HTTP + +Hello from upstream server server1 + +WebSocket + +Hello from upstream server server2! +Hello from upstream server server2! +Hello from upstream server server2! +``` + +## Source code + +### Upstream server + +```go +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "golang.org/x/net/websocket" +) + +var index = ` + + + + + + + Upstream Server + + + +

HTTP

+

+ Hello from upstream server %s +

+

WebSocket

+

+ + + +` + +func main() { + name := os.Args[1] + port := os.Args[2] + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) + }) + + // WebSocket handler + e.GET("/ws", func(c *echo.Context) error { + websocket.Handler(func(ws *websocket.Conn) { + defer ws.Close() + for { + // Write + err := websocket.Message.Send(ws, fmt.Sprintf("Hello from upstream server %s!", name)) + if err != nil { + e.Logger.Error("failed to send message", "error", err) + } + select { + case <-ws.Request().Context().Done(): + return + case <-time.After(1 * time.Second): + continue + } + } + }).ServeHTTP(c.Response(), c.Request()) + return nil + }) + + sc := echo.StartConfig{Address: port} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Proxy server + +```go +package main + +import ( + "context" + "net/url" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Setup proxy + url1, _ := url.Parse("http://localhost:8081") + url2, _ := url.Parse("http://localhost:8082") + targets := []*middleware.ProxyTarget{ + {URL: url1}, + {URL: url2}, + } + e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/sse.md b/site/src/content/docs/cookbook/sse.md new file mode 100644 index 00000000..2227f4af --- /dev/null +++ b/site/src/content/docs/cookbook/sse.md @@ -0,0 +1,305 @@ +--- +title: Server-Sent Events (SSE) +description: Stream server-sent events from an Echo handler, either per connection or broadcast to many clients. +sidebar: + order: 14 +--- + +[Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) +can be used in several ways. The first example below is per-connection, per-handler +SSE. For more complex broadcasting logic, see the second example using +[r3labs/sse](https://github.com/r3labs/sse). + +:::caution +SSE connections are long-lived, so the server's write timeout must be disabled. +Both examples set `s.WriteTimeout = 0` via `BeforeServeFunc`. +::: + +## Using SSE + +### Server + +The handler writes the SSE headers, then emits an event every second until the +client disconnects. `http.NewResponseController(w).Flush()` pushes each event to +the client immediately. + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + e.File("/", "./index.html") + + e.GET("/sse", func(c *echo.Context) error { + log.Printf("SSE client connected, ip: %v", c.RealIP()) + + w := c.Response() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + count := uint64(0) + for { + select { + case <-c.Request().Context().Done(): + log.Printf("SSE client disconnected, ip: %v", c.RealIP()) + return nil + case <-ticker.C: + count++ + event := Event{ + Data: []byte(fmt.Sprintf("count: %d, time: %s\n\n", count, time.Now().Format(time.RFC3339Nano))), + } + if err := event.MarshalTo(w); err != nil { + return err + } + if err := http.NewResponseController(w).Flush(); err != nil { + return err + } + } + } + }) + + sc := echo.StartConfig{ + Address: ":8080", + BeforeServeFunc: func(s *http.Server) error { + s.WriteTimeout = 0 // IMPORTANT: disable for SSE + return nil + }, + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c + defer cancel() + + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Event structure and Marshal method + +```go +package main + +import ( + "bytes" + "fmt" + "io" +) + +// Event represents Server-Sent Event. +// SSE explanation: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format +type Event struct { + // ID is used to set the EventSource object's last event ID value. + ID []byte + // Data field is for the message. When the EventSource receives multiple consecutive lines + // that begin with data:, it concatenates them, inserting a newline character between each one. + // Trailing newlines are removed. + Data []byte + // Event is a string identifying the type of event described. If this is specified, an event + // will be dispatched on the browser to the listener for the specified event name; the website + // source code should use addEventListener() to listen for named events. The onmessage handler + // is called if no event name is specified for a message. + Event []byte + // Retry is the reconnection time. If the connection to the server is lost, the browser will + // wait for the specified time before attempting to reconnect. This must be an integer, specifying + // the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored. + Retry []byte + // Comment line can be used to prevent connections from timing out; a server can send a comment + // periodically to keep the connection alive. + Comment []byte +} + +// MarshalTo marshals Event to given Writer +func (ev *Event) MarshalTo(w io.Writer) error { + // Marshalling part is taken from: https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/http.go#L16 + if len(ev.Data) == 0 && len(ev.Comment) == 0 { + return nil + } + + if len(ev.Data) > 0 { + if _, err := fmt.Fprintf(w, "id: %s\n", ev.ID); err != nil { + return err + } + + sd := bytes.Split(ev.Data, []byte("\n")) + for i := range sd { + if _, err := fmt.Fprintf(w, "data: %s\n", sd[i]); err != nil { + return err + } + } + + if len(ev.Event) > 0 { + if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil { + return err + } + } + + if len(ev.Retry) > 0 { + if _, err := fmt.Fprintf(w, "retry: %s\n", ev.Retry); err != nil { + return err + } + } + } + + if len(ev.Comment) > 0 { + if _, err := fmt.Fprintf(w, ": %s\n", ev.Comment); err != nil { + return err + } + } + + if _, err := fmt.Fprint(w, "\n"); err != nil { + return err + } + + return nil +} +``` + +### HTML serving SSE + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` + +## Broadcasting with r3labs/sse + +When you need to broadcast a single stream of events to many subscribers, the +[r3labs/sse](https://github.com/r3labs/sse) library handles stream and subscriber +management for you. + +### Server + +```go +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/r3labs/sse/v2" +) + +func main() { + e := echo.New() + + server := sse.New() // create SSE broadcaster server + server.AutoReplay = false // do not replay messages for each new subscriber that connects + _ = server.CreateStream("time") // EventSource in "index.html" connecting to stream named "time" + + go func(s *sse.Server) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + s.Publish("time", &sse.Event{ + Data: []byte("time: " + time.Now().Format(time.RFC3339Nano)), + }) + } + } + }(server) + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + e.File("/", "./index.html") + + //e.GET("/sse", echo.WrapHandler(server)) + + e.GET("/sse", func(c *echo.Context) error { // longer variant with disconnect logic + e.Logger.Info("New client connected", "ip", c.RealIP()) + go func() { + <-c.Request().Context().Done() // Received Browser Disconnection + e.Logger.Info("Client disconnected", "ip", c.RealIP()) + }() + + server.ServeHTTP(c.Response(), c.Request()) + return nil + }) + + sc := echo.StartConfig{ + Address: ":8080", + BeforeServeFunc: func(s *http.Server) error { + s.WriteTimeout = 0 // IMPORTANT: disable for SSE + return nil + }, + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c + defer cancel() + + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### HTML serving SSE + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` diff --git a/site/src/content/docs/cookbook/streaming-response.md b/site/src/content/docs/cookbook/streaming-response.md new file mode 100644 index 00000000..98c10613 --- /dev/null +++ b/site/src/content/docs/cookbook/streaming-response.md @@ -0,0 +1,95 @@ +--- +title: Streaming Response +description: Stream data to the client as it is produced using chunked transfer encoding. +sidebar: + order: 15 +--- + +This recipe streams a JSON response to the client as each record is produced, +using chunked transfer encoding: + +- Send data as it is produced. +- Stream a JSON response with chunked transfer encoding. + +The handler encodes one record at a time and calls +`http.NewResponseController(...).Flush()` after each to push it to the client +immediately, pausing one second between records. + +## Server + +```go +package main + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/labstack/echo/v5" +) + +type ( + Geolocation struct { + Altitude float64 + Latitude float64 + Longitude float64 + } +) + +var ( + locations = []Geolocation{ + {-97, 37.819929, -122.478255}, + {1899, 39.096849, -120.032351}, + {2619, 37.865101, -119.538329}, + {42, 33.812092, -117.918974}, + {15, 37.77493, -122.419416}, + } +) + +func main() { + e := echo.New() + e.GET("/", func(c *echo.Context) error { + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + c.Response().WriteHeader(http.StatusOK) + + enc := json.NewEncoder(c.Response()) + for _, l := range locations { + if err := enc.Encode(l); err != nil { + return err + } + if err := http.NewResponseController(c.Response()).Flush(); err != nil { + return err + } + select { + case <-c.Request().Context().Done(): + return nil + case <-time.After(1 * time.Second): + continue + } + } + return nil + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Client + +```sh +curl localhost:1323 +``` + +### Output + +```js +{"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255} +{"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351} +{"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329} +{"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974} +{"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416} +``` diff --git a/site/src/content/docs/cookbook/subdomain.md b/site/src/content/docs/cookbook/subdomain.md new file mode 100644 index 00000000..050beab0 --- /dev/null +++ b/site/src/content/docs/cookbook/subdomain.md @@ -0,0 +1,78 @@ +--- +title: Subdomain +description: Route requests to different Echo instances per host using a virtual host handler. +sidebar: + order: 17 +--- + +This recipe routes requests to separate `Echo` instances based on the request +host, so each subdomain has its own routes and middleware. The instances are +combined with `echo.NewVirtualHostHandler`, which dispatches by host name. + +## Server + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + // Hosts + vHosts := make(map[string]*echo.Echo) + + //----- + // API + //----- + + api := echo.New() + api.Use(middleware.RequestLogger()) + api.Use(middleware.Recover()) + + vHosts["api.localhost:1323"] = api + + api.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "API") + }) + + //------ + // Blog + //------ + + blog := echo.New() + blog.Use(middleware.RequestLogger()) + blog.Use(middleware.Recover()) + + vHosts["blog.localhost:1323"] = blog + + blog.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Blog") + }) + + //--------- + // Website + //--------- + + site := echo.New() + site.Use(middleware.RequestLogger()) + site.Use(middleware.Recover()) + + vHosts["localhost:1323"] = site + + site.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Website") + }) + + e := echo.NewVirtualHostHandler(vHosts) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/timeout.md b/site/src/content/docs/cookbook/timeout.md new file mode 100644 index 00000000..df8f8a95 --- /dev/null +++ b/site/src/content/docs/cookbook/timeout.md @@ -0,0 +1,53 @@ +--- +title: Timeout +description: Apply a request timeout to handlers with the ContextTimeout middleware. +sidebar: + order: 18 +--- + +The [`ContextTimeout`](/middleware/context-timeout/) middleware sets a deadline on the +request's `context.Context`. When the deadline passes, the context is cancelled, +and handlers that watch `c.Request().Context().Done()` can return promptly instead +of running to completion. + +In the example below the middleware imposes a 5-second timeout while the handler +would otherwise take 10 seconds, so the request returns a `408 Request Timeout`. + +## Server + +```go +package main + +import ( + "context" + "net/http" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.ContextTimeout(5 * time.Second)) + + // Route => handler + e.GET("/", func(c *echo.Context) error { + select { + case <-c.Request().Context().Done(): + return echo.NewHTTPError(http.StatusRequestTimeout, "Request timed out") + case <-time.After(10 * time.Second): + return c.String(http.StatusOK, "Hello, World!\n") + } + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/cookbook/websocket.md b/site/src/content/docs/cookbook/websocket.md new file mode 100644 index 00000000..06bcab36 --- /dev/null +++ b/site/src/content/docs/cookbook/websocket.md @@ -0,0 +1,189 @@ +--- +title: WebSocket +description: Handle WebSocket connections in Echo using golang.org/x/net/websocket or gorilla/websocket. +sidebar: + order: 16 +--- + +Echo handlers can serve WebSocket connections by upgrading the underlying +HTTP connection. This recipe shows two approaches: the standard +`golang.org/x/net/websocket` package and the popular +[`gorilla/websocket`](https://github.com/gorilla/websocket) library. + +## Using net WebSocket + +### Server + +```go +package main + +import ( + "context" + "fmt" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "golang.org/x/net/websocket" +) + +func hello(c *echo.Context) error { + websocket.Handler(func(ws *websocket.Conn) { + defer ws.Close() + for { + // Write + if err := websocket.Message.Send(ws, "Hello, Client!"); err != nil { + c.Logger().Error("failed to write WS message", "error", err) + } + + // Read + msg := "" + if err := websocket.Message.Receive(ws, &msg); err != nil { + c.Logger().Error("failed to write WS message", "error", err) + } + fmt.Printf("%s\n", msg) + } + }).ServeHTTP(c.Response(), c.Request()) + return nil +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "../public") + e.GET("/ws", hello) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Using gorilla WebSocket + +### Server + +```go +package main + +import ( + "context" + "fmt" + + "github.com/gorilla/websocket" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var ( + upgrader = websocket.Upgrader{} +) + +func hello(c *echo.Context) error { + ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + return err + } + defer ws.Close() + + for { + // Write + err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!")) + if err != nil { + c.Logger().Error("failed to write WS message", "error", err) + } + + // Read + _, msg, err := ws.ReadMessage() + if err != nil { + c.Logger().Error("failed to read WS message", "error", err) + } + fmt.Printf("%s\n", msg) + } +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "../public") + + e.GET("/ws", hello) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Client + +```html + + + + + + WebSocket + + + +

+ + + + + +``` + +## Output + +**Server** + +```sh +Hello, Server! +Hello, Server! +Hello, Server! +Hello, Server! +Hello, Server! +``` + +**Client** + +```sh +Hello, Client! +Hello, Client! +Hello, Client! +Hello, Client! +Hello, Client! +``` diff --git a/site/src/content/docs/guide/binding.md b/site/src/content/docs/guide/binding.md new file mode 100644 index 00000000..894cd411 --- /dev/null +++ b/site/src/content/docs/guide/binding.md @@ -0,0 +1,151 @@ +--- +title: Binding +description: Parse request data into typed Go structs from path, query, header, and body. +sidebar: + order: 5 +--- + +Parsing request data is a crucial part of a web application. In Echo this is called +_binding_, and it can read from four parts of an HTTP request: + +- URL path parameters +- URL query parameters +- Headers +- Request body + +## Struct tag binding + +Define a struct with tags specifying the data source and key, then call `c.Bind()` +with a pointer to it. Here the query parameter `id` binds to the `ID` field: + +```go +type User struct { + ID string `query:"id"` +} + +// handler for /users?id= +var user User +if err := c.Bind(&user); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +``` + +### Data sources + +| Tag | Source | +| -------- | ------ | +| `query` | Query parameter | +| `param` | Path parameter | +| `header` | Header value | +| `form` | Form data (query + body) | +| `json` | Request body (`encoding/json`) | +| `xml` | Request body (`encoding/xml`) | + +Path, query, header, and form fields require an **explicit tag**. JSON and XML fall +back to the struct field name when the tag is omitted, matching the standard library. + +### Body content types + +When decoding the request body, the `Content-Type` header selects the decoder: + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### Multiple sources & precedence + +A field can declare several sources. Data is bound in this order, each step +overwriting the previous: + +1. Path parameters +2. Query parameters (GET / DELETE only) +3. Request body + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### Direct binding from one source + +```go +echo.BindBody(c, &payload) // request body +echo.BindQueryParams(c, &payload) // query parameters +echo.BindPathValues(c, &payload) // path parameters +echo.BindHeaders(c, &payload) // headers +``` + +:::note +Headers are **not** included by `c.Bind()`. Bind them with `echo.BindHeaders` directly. +::: + +:::caution[Security] +Don't bind directly into business structs. If a bound struct exposed an `IsAdmin bool` +field, a request body of `{"IsAdmin": true}` would set it. Use a dedicated DTO and map +it explicitly: +::: + +```go +type UserDTO struct { + Name string `json:"name" form:"name" query:"name"` + Email string `json:"email" form:"email" query:"email"` +} + +e.POST("/users", func(c *echo.Context) error { + var dto UserDTO + if err := c.Bind(&dto); err != nil { + return c.String(http.StatusBadRequest, "bad request") + } + user := User{Name: dto.Name, Email: dto.Email, IsAdmin: false} + executeSomeBusinessLogic(user) + return c.JSON(http.StatusOK, user) +}) +``` + +## Fluent binding + +For explicit, type-safe binding from a single source, use the fluent binders. They +chain configuration and execute, collecting errors: + +```go +// /api/search?active=true&id=1&id=2&id=3&length=25 +var opts struct { + IDs []int64 + Active bool +} +length := int64(50) + +err := echo.QueryParamsBinder(c). + Int64("length", &length). + Int64s("id", &opts.IDs). + Bool("active", &opts.Active). + BindError() // first error, if any +``` + +Available binders: `echo.QueryParamsBinder(c)`, `echo.PathValuesBinder(c)`, +`echo.FormFieldBinder(c)`. End a chain with `BindError()` (first error) or +`BindErrors()` (all errors). `FailFast(false)` runs the whole chain; it's on by default. + +Each supported type offers `Type(...)`, `MustType(...)`, `Types(...)` (slices), and +`MustTypes(...)` methods — e.g. `Int64`, `MustInt64`, `Int64s`. Use +`BindWithDelimiter("id", &dest, ",")` to split comma-joined values. + +## Custom binder + +Register a custom binder via `Echo#Binder`: + +```go +type CustomBinder struct{} + +func (cb *CustomBinder) Bind(c *echo.Context, i any) error { + db := new(echo.DefaultBinder) + if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType { + return err + } + // custom logic here + return nil +} + +e.Binder = &CustomBinder{} +``` diff --git a/site/src/content/docs/guide/context.md b/site/src/content/docs/guide/context.md new file mode 100644 index 00000000..2b4699d3 --- /dev/null +++ b/site/src/content/docs/guide/context.md @@ -0,0 +1,78 @@ +--- +title: Context +description: The per-request object carrying the request, response, params, and helpers. +sidebar: + order: 4 +--- + +`echo.Context` represents the context of the current HTTP request. A pointer to it +(`*echo.Context`) is passed to every handler and middleware, carrying the request and +response, path parameters, bound data, and helpers for building responses. + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## Reading input + +```go +id := c.Param("id") // path parameter +q := c.QueryParam("q") // query string value +all := c.QueryParams() // url.Values of all query params +name := c.FormValue("name") // form field (URL + body) +ua := c.Request().Header.Get(echo.HeaderUserAgent) +``` + +There are matching `*Or` helpers that return a default when a value is absent — +`c.ParamOr("id", "0")`, `c.QueryParamOr("page", "1")`, `c.FormValueOr(...)`. + +## Writing responses + +```go +c.String(http.StatusOK, "plain text") +c.JSON(http.StatusOK, payload) +c.JSONPretty(http.StatusOK, payload, " ") +c.HTML(http.StatusOK, "hi") +c.XML(http.StatusOK, payload) +c.Blob(http.StatusOK, "application/pdf", bytes) +c.Stream(http.StatusOK, "application/octet-stream", reader) +c.NoContent(http.StatusNoContent) +c.Redirect(http.StatusFound, "/elsewhere") +``` + +## Files + +```go +c.File("public/report.pdf") // serve a file +c.Attachment("invoice.pdf", "inv.pdf") // prompt download +c.Inline("photo.png", "photo.png") // render inline +``` + +## Per-request storage + +Share data between middleware and handlers with `Get`/`Set`: + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +Typed access is available via the generics helpers: + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## Binding & validation + +`c.Bind()` parses request data into a struct; see [Binding](/guide/binding/). + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/guide/cookies.md b/site/src/content/docs/guide/cookies.md new file mode 100644 index 00000000..fec956f9 --- /dev/null +++ b/site/src/content/docs/guide/cookies.md @@ -0,0 +1,72 @@ +--- +title: Cookies +description: Create, read, and list HTTP cookies using the standard http.Cookie type. +sidebar: + order: 11 +--- + +A cookie is a small piece of data a server sends to the browser, which the browser +stores and sends back on subsequent requests. Cookies let websites remember stateful +information such as a shopping cart, authentication state, or previously entered form +values. + +Echo uses Go's standard `http.Cookie` type to add and retrieve cookies from the +`echo.Context` in a handler. + +## Cookie attributes + +| Attribute | Optional | +| ---------- | -------- | +| `Name` | No | +| `Value` | No | +| `Path` | Yes | +| `Domain` | Yes | +| `Expires` | Yes | +| `Secure` | Yes | +| `HttpOnly` | Yes | + +## Create a cookie + +```go +func writeCookie(c *echo.Context) error { + cookie := new(http.Cookie) + cookie.Name = "username" + cookie.Value = "jon" + cookie.Expires = time.Now().Add(24 * time.Hour) + c.SetCookie(cookie) + return c.String(http.StatusOK, "write a cookie") +} +``` + +- Create the cookie with `new(http.Cookie)`. +- Set attributes on the `http.Cookie` fields. +- Call `c.SetCookie(cookie)` to add a `Set-Cookie` header to the response. + +## Read a cookie + +```go +func readCookie(c *echo.Context) error { + cookie, err := c.Cookie("username") + if err != nil { + return err + } + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + return c.String(http.StatusOK, "read a cookie") +} +``` + +- Read a cookie by name with `c.Cookie("username")`. +- Access its attributes through the `http.Cookie` fields. + +## Read all cookies + +```go +func readAllCookies(c *echo.Context) error { + for _, cookie := range c.Cookies() { + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + } + return c.String(http.StatusOK, "read all the cookies") +} +``` diff --git a/site/src/content/docs/guide/customization.md b/site/src/content/docs/guide/customization.md new file mode 100644 index 00000000..72e98796 --- /dev/null +++ b/site/src/content/docs/guide/customization.md @@ -0,0 +1,63 @@ +--- +title: Customization +description: Customize Echo's logger, validator, binder, renderer, serializer, and error handling. +sidebar: + order: 12 +--- + +Echo exposes a set of fields on the `Echo` instance that let you replace built-in +behavior with your own implementations. + +## Logging + +`Echo#Logger` writes structured logs. The default handler emits JSON to `os.Stdout`. + +### Custom logger + +The logger is an `*slog.Logger`, so you can register any `slog` handler: + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` registers a validator for request payload validation. + +[Learn more](/guide/request/#validate-data) + +## Custom binder + +`Echo#Binder` registers a custom binder for binding request payloads. + +[Learn more](/guide/binding/#custom-binder) + +## Custom JSON serializer + +`Echo#JSONSerializer` registers a custom JSON serializer. See `DefaultJSONSerializer` +in [json.go](https://github.com/labstack/echo/blob/master/json.go). + +## Renderer + +`Echo#Renderer` registers a renderer for template rendering. + +[Learn more](/guide/templates/) + +## HTTP error handler + +`Echo#HTTPErrorHandler` registers a custom HTTP error handler. + +[Learn more](/guide/error-handling/) + +## Route callback + +`Echo#OnAddRoute` registers a callback invoked whenever a new route is added to the +router. + +## IP extractor + +`Echo#IPExtractor` controls how the real client IP address is determined. To +retrieve it reliably and securely, your application must be aware of your entire +infrastructure. + +[Learn more](/guide/ip-address/) diff --git a/site/src/content/docs/guide/error-handling.md b/site/src/content/docs/guide/error-handling.md new file mode 100644 index 00000000..f754fe72 --- /dev/null +++ b/site/src/content/docs/guide/error-handling.md @@ -0,0 +1,82 @@ +--- +title: Error Handling +description: Centralized HTTP error handling by returning errors from handlers and middleware. +sidebar: + order: 6 +--- + +Echo advocates **centralized** error handling: handlers and middleware return an +`error`, and a single error handler turns it into an HTTP response. This keeps logging +and response formatting in one place. + +Return a plain `error` or an `*echo.HTTPError`: + +```go +e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + if !authenticated(c) { + // invalid credentials → abort with 401 + return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials") + } + return next(c) + } +}) +``` + +`echo.NewHTTPError(code)` without a message uses the status text (e.g. `"Unauthorized"`). +Echo also ships sentinel errors like `echo.ErrBadRequest`, `echo.ErrNotFound`, and +`echo.ErrUnauthorized`. + +## Default error handler + +Echo's default handler responds in JSON: + +```json +{ "message": "error connecting to redis" } +``` + +A plain `error` becomes `500 Internal Server Error` (the original message is included +when running with errors exposed). An `*HTTPError` uses its status code and message. + +## Custom error handler + +Set your own via `e.HTTPErrorHandler` — useful for error pages, notifications, or +sending errors to a centralized system. + +Check whether the response was already sent with `echo.UnwrapResponse()`, and find a +status code in the error chain via `echo.HTTPStatusCoder`: + +```go +func customHTTPErrorHandler(c *echo.Context, err error) { + if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + if resp.Committed { + return // already sent by a handler/middleware + } + } + + code := http.StatusInternalServerError + var sc echo.HTTPStatusCoder + if errors.As(err, &sc) { + if tmp := sc.StatusCode(); tmp != 0 { + code = tmp + } + } + + var cErr error + if c.Request().Method == http.MethodHead { + cErr = c.NoContent(code) + } else { + cErr = c.File(fmt.Sprintf("%d.html", code)) // e.g. 404.html, 500.html + } + if cErr != nil { + c.Logger().Error("failed to send error page", "error", errors.Join(err, cErr)) + } +} + +e.HTTPErrorHandler = customHTTPErrorHandler +``` + +:::tip +Instead of (or in addition to) the logger, forward errors to an external service like +Sentry, Elasticsearch, or Splunk from the central handler. +::: diff --git a/site/src/content/docs/guide/installation.md b/site/src/content/docs/guide/installation.md new file mode 100644 index 00000000..e8a47883 --- /dev/null +++ b/site/src/content/docs/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: Installation +description: Add Echo v5 to your Go module. +sidebar: + order: 2 +--- + +Echo is distributed as a Go module: `github.com/labstack/echo/v5`. + +## Requirements + +Echo v5 requires **Go 1.25 or newer**. + +```bash +go version +``` + +## Add to a project + +Inside an existing module: + +```bash +go get github.com/labstack/echo/v5 +``` + +Or start a new module: + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +Import it in your code: + +```go +import "github.com/labstack/echo/v5" +``` + +## Versions + +| Version | Import path | Status | +| ------- | ------------------------------- | ------ | +| **v5** | `github.com/labstack/echo/v5` | Current | +| v4 | `github.com/labstack/echo/v4` | LTS (maintenance) | + +:::note +Echo follows [semantic import versioning](https://go.dev/blog/v2-go-modules) — the +major version is part of the import path, so v4 and v5 can coexist during a migration. +::: + +## Keeping up to date + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/guide/ip-address.md b/site/src/content/docs/guide/ip-address.md new file mode 100644 index 00000000..fb5f9542 --- /dev/null +++ b/site/src/content/docs/guide/ip-address.md @@ -0,0 +1,118 @@ +--- +title: IP Address +description: Retrieve the real client IP address securely behind proxies. +sidebar: + order: 14 +--- + +The IP address plays a fundamental role in HTTP — it is used for access control, +auditing, geo-based analysis, and more. Echo exposes `Context#RealIP()` to retrieve +it. + +Retrieving the _real_ client IP is not trivial, especially when L7 proxies sit in +front of your application. In that case the real IP must be relayed over HTTP from +the proxies to your app — but you must not trust HTTP headers unconditionally, or you +risk being deceived. **This is a security risk.** + +To retrieve the IP reliably and securely, your application must be aware of your +entire infrastructure. In Echo, you configure this through `Echo#IPExtractor`. + +:::caution +If you do not set `Echo#IPExtractor` explicitly, Echo falls back to legacy behavior, +which is not a secure default. +::: + +Start with two questions to find the right approach: + +1. Do you put any HTTP (L7) proxy in front of the application? This includes cloud + load balancers (such as AWS ALB or GCP HTTP LB) and open-source proxies (such as + Nginx, Envoy, or an Istio ingress gateway). +2. If so, which HTTP header do your proxies use to pass the client IP to the + application? + +## Case 1: No proxy + +If there is no proxy (the app faces the internet directly), the only address you can +trust is the one from the network layer. Every HTTP header is untrustworthy because +clients have full control over them. + +Use `echo.ExtractIPDirect()`: + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## Case 2: Proxies using the X-Forwarded-For header + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +is the most common header for relaying client IPs. At each hop, the proxy appends the +request IP to the end of the header. + +```text + ┌──────────┐ ┌──────────┐ ┌──────────┐ +───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ + │ (IP: b) │ │ (IP: c) │ │ │ + └──────────┘ └──────────┘ └──────────┘ + +Case 1. +XFF: "" "a" "a, b" + ~~~~~~ +Case 2. +XFF: "x" "x, a" "x, a, b" + ~~~~~~~~~ + ↑ What your app will see +``` + +In this case, take the **first untrustworthy IP reading from the right**. Never take +the first one from the left, since the client controls it. Here "trustworthy" means +you are sure the IP belongs to your infrastructure. In the example above, if `b` and +`c` are trustworthy, the client IP is `a` in both cases — never `x`. + +Use `ExtractIPFromXFFHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +By default it trusts internal IP addresses — loopback, link-local unicast, +private-use, and unique local addresses from +[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890), +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291), and +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193). Control this with `TrustOption`s: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader( + echo.TrustLoopback(false), // e.g. IPv4 starting with 127. + echo.TrustLinkLocal(false), // e.g. IPv4 starting with 169.254. + echo.TrustPrivateNet(false), // e.g. IPv4 starting with 10. or 192.168. + echo.TrustIPRange(lbIPRange), +) +``` + +## Case 3: Proxies using the X-Real-IP header + +`X-Real-IP` is another header for relaying the client IP, but unlike XFF it carries +only a single address. + +If your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +As with XFF, it trusts internal IP addresses by default and accepts the same +`TrustOption`s. + +:::danger +**Never forget** to configure the outermost proxy (at the edge of your +infrastructure) **not to pass through incoming headers**. Otherwise a client can +forge them, opening the door to fraud. +::: + +## Default behavior + +By default, Echo considers the first XFF header, the X-Real-IP header, and the IP +from the network layer all at once. + +As this article should make clear, that is not a good choice. It remains the default +only for backward compatibility. diff --git a/site/src/content/docs/guide/quickstart.md b/site/src/content/docs/guide/quickstart.md new file mode 100644 index 00000000..e780b0c1 --- /dev/null +++ b/site/src/content/docs/guide/quickstart.md @@ -0,0 +1,76 @@ +--- +title: Quickstart +description: Build a production-ready Echo API in under five minutes. +sidebar: + order: 1 +--- + +Echo is a high performance, minimalist Go web framework. This guide gets a server +running in under five minutes. + +## Requirements + +Echo requires **Go 1.25 or newer**. Check your version: + +```bash +go version +``` + +## Install + +Create a module and add Echo: + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +Create `main.go`: + +```go +package main + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"message": "Hello, World!"}) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Run it: + +```bash +go run main.go +``` + +Your server is live at `http://localhost:1323`. Echo's router dispatches requests +with **zero dynamic memory allocation** per route. + +:::tip[Ask Echo] +Stuck? Press the **Ask Echo** button (bottom-right) and ask +*"How do I add JWT auth?"* — answers come straight from these docs. +::: + +## Next steps + +- [Routing](/guide/routing/) — static, parameterized, and wildcard routes. +- [Context](/guide/context/) — the per-request request/response object. +- [Binding](/guide/binding/) — parse request data into typed structs. diff --git a/site/src/content/docs/guide/request.md b/site/src/content/docs/guide/request.md new file mode 100644 index 00000000..033998ce --- /dev/null +++ b/site/src/content/docs/guide/request.md @@ -0,0 +1,170 @@ +--- +title: Request +description: Retrieve form, query, and path data from a request, and validate it. +sidebar: + order: 7 +--- + +A handler reads request data through the `echo.Context`. Echo can retrieve values +individually by name, bind them into structs (see [Binding](/guide/binding/)), and +hand off validation to a validator you register. + +## Retrieve data + +### Form data + +Retrieve a form field by name with `Context#FormValue(name string)`: + +```go +e.POST("/form", func(c *echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +For types other than `string`, use the generic `echo.FormValue[T]` function: + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +Test with: + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +To bind a custom data type, implement the `echo.BindUnmarshaler` interface: + +```go +type Timestamp time.Time + +func (t *Timestamp) UnmarshalParam(src string) error { + ts, err := time.Parse(time.RFC3339, src) + if err != nil { + return err + } + *t = Timestamp(ts) + return nil +} +``` + +### Query parameters + +Retrieve a query parameter by name with `Context#QueryParam(name string)`: + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +For types other than `string`, use the generic `echo.QueryParam[T]` function: + +```go +age, err := echo.QueryParam[int](c, "age") +if err != nil { + return err +} +``` + +```sh +curl -X GET "http://localhost:1323?name=Joe&age=30" +``` + +### Path parameters + +Retrieve a registered path parameter by name with `Context#Param(name string)`: + +```go +e.GET("/users/:name", func(c *echo.Context) error { + name := c.Param("name") + return c.String(http.StatusOK, name) +}) +``` + +For types other than `string`, use the generic `echo.PathParam[T]` function: + +```go +id, err := echo.PathParam[int](c, "id") +if err != nil { + return err +} +``` + +```sh +curl http://localhost:1323/users/Joe +curl http://localhost:1323/users/123 +``` + +### Binding data + +Echo can also bind request data into native Go structs and variables. See +[Binding](/guide/binding/). + +## Validate data + +Echo has no built-in data validation. You can register a custom validator via +`Echo#Validator` and use a third-party library such as +[go-playground/validator](https://github.com/go-playground/validator). + +The example below validates a bound struct: + +```go +package main + +import ( + "net/http" + + "github.com/go-playground/validator/v10" // go get github.com/go-playground/validator/v10 + "github.com/labstack/echo/v5" +) + +type CustomValidator struct { + validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i any) error { + if err := cv.validator.Struct(i); err != nil { + // Optionally return the error to let each route control the status code. + return echo.ErrBadRequest.Wrap(err) + } + return nil +} + +type User struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` +} + +func main() { + e := echo.New() + e.Validator = &CustomValidator{validator: validator.New()} + + e.POST("/users", func(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + if err := c.Validate(u); err != nil { + return err + } + return c.JSON(http.StatusOK, u) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +```sh +curl -X POST http://localhost:1323/users \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe","email":"joe@invalid-domain"}' +{"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"} +``` diff --git a/site/src/content/docs/guide/response.md b/site/src/content/docs/guide/response.md new file mode 100644 index 00000000..04b64819 --- /dev/null +++ b/site/src/content/docs/guide/response.md @@ -0,0 +1,322 @@ +--- +title: Response +description: Send strings, HTML, JSON, XML, files, streams, redirects, and response hooks. +sidebar: + order: 8 +--- + +A handler writes its response through the `echo.Context`. Each helper sets the +appropriate `Content-Type` and status code for you. + +## Send string + +`Context#String(code int, s string)` sends a plain text response with a status code. + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## Send HTML + +`Context#HTML(code int, html string)` sends a simple HTML response with a status +code. To generate HTML dynamically, see [Templates](/guide/templates/). + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### Send HTML blob + +`Context#HTMLBlob(code int, b []byte)` sends an HTML blob with a status code. It is +handy with a template engine that outputs `[]byte`. + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## Render template + +See [Templates](/guide/templates/). + +## Send JSON + +`Context#JSON(code int, i any)` encodes a Go value as JSON and sends it with a +status code. + +```go +type User struct { + Name string `json:"name" xml:"name"` + Email string `json:"email" xml:"email"` +} + +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSON(http.StatusOK, u) +} +``` + +### Stream JSON + +`Context#JSON()` uses `json.Marshal` internally, which may be inefficient for large +payloads. In that case, stream the JSON directly: + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + return json.NewEncoder(c.Response()).Encode(u) +} +``` + +### JSON pretty + +`Context#JSONPretty(code int, i any, indent string)` sends a pretty-printed JSON +response. The indent can be spaces or tabs. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSONPretty(http.StatusOK, u, " ") +} +``` + +```json +{ + "email": "jon@labstack.com", + "name": "Jon" +} +``` + +### JSON blob + +`Context#JSONBlob(code int, b []byte)` sends a pre-encoded JSON blob directly, for +example from a database. + +```go +func(c *echo.Context) error { + encodedJSON := []byte{} // Encoded JSON from an external source. + return c.JSONBlob(http.StatusOK, encodedJSON) +} +``` + +## Send JSONP + +`Context#JSONP(code int, callback string, i any)` encodes a Go value as JSON and +sends it as a JSONP payload wrapped in the given callback. + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +See the [JSONP cookbook](/cookbook/jsonp/). + +## Send XML + +`Context#XML(code int, i any)` encodes a Go value as XML and sends it with a status +code. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XML(http.StatusOK, u) +} +``` + +### Stream XML + +`Context#XML` uses `xml.Marshal` internally, which may be inefficient for large +payloads. In that case, stream the XML directly: + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + return xml.NewEncoder(c.Response()).Encode(u) +} +``` + +### XML pretty + +`Context#XMLPretty(code int, i any, indent string)` sends a pretty-printed XML +response. The indent can be spaces or tabs. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XMLPretty(http.StatusOK, u, " ") +} +``` + +```xml + + + Jon + jon@labstack.com + +``` + +:::tip +You can also make `Context#XML()` output pretty-printed XML by appending `pretty` +to the request URL query string. + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### XML blob + +`Context#XMLBlob(code int, b []byte)` sends a pre-encoded XML blob directly, for +example from a database. + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## Send file + +`Context#File(file string)` sends the contents of a file as the response. It sets +the correct content type and handles caching automatically. + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## Send attachment + +`Context#Attachment(file, name string)` is like `File()` but sends the file with +`Content-Disposition: attachment` and the given name. + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## Send inline + +`Context#Inline(file, name string)` is like `File()` but sends the file with +`Content-Disposition: inline` and the given name. + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## Send blob + +`Context#Blob(code int, contentType string, b []byte)` sends arbitrary data with a +given content type and status code. + +```go +func(c *echo.Context) error { + data := []byte(`0306703,0035866,NO_ACTION,06/19/2006 +0086003,"0005866",UPDATED,06/19/2006`) + return c.Blob(http.StatusOK, "text/csv", data) +} +``` + +## Send stream + +`Context#Stream(code int, contentType string, r io.Reader)` sends an arbitrary data +stream with a given content type, `io.Reader`, and status code. + +```go +func(c *echo.Context) error { + f, err := os.Open("") + if err != nil { + return err + } + defer f.Close() + return c.Stream(http.StatusOK, "image/png", f) +} +``` + +## Send no content + +`Context#NoContent(code int)` sends an empty body with a status code. + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## Redirect request + +`Context#Redirect(code int, url string)` redirects the request to the given URL with +a status code. + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## Hooks + +### Before response + +`Response#Before(func())` registers a function that runs just before the response is +written. + +### After response + +`Response#After(func())` registers a function that runs just after the response is +written. If the `Content-Length` is unknown, no after functions run. + +```go +e.GET("/hooks", func(c *echo.Context) error { + resp, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return err + } + resp.Before(func() { + println("before response") + }) + resp.After(func() { + println("after response") + }) + return c.String(http.StatusOK, "Hello, World!") +}) +``` + +:::tip +You can register multiple `Before` and `After` functions. +::: diff --git a/site/src/content/docs/guide/routing.md b/site/src/content/docs/guide/routing.md new file mode 100644 index 00000000..cb3c28cd --- /dev/null +++ b/site/src/content/docs/guide/routing.md @@ -0,0 +1,75 @@ +--- +title: Routing +description: Match request URLs to handlers on Echo's zero-allocation radix tree. +sidebar: + order: 3 +--- + +Echo's optimized router matches request URLs to handlers using a radix tree with +**zero dynamic memory allocation** and smart route prioritization. + +## Registering routes + +Use the HTTP-method helpers on the `Echo` instance. Each takes a path pattern and a +`HandlerFunc` (`func(c *echo.Context) error`), with optional route-level middleware. + +```go +e := echo.New() + +e.GET("/users/:id", getUser) // named parameter +e.POST("/users", createUser) +e.PUT("/users/:id", updateUser) +e.DELETE("/users/:id", deleteUser) +e.GET("/static/*", serveFiles) // wildcard +``` + +`Any` registers a handler for all supported methods, and `Match` for a specific set: + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## Match types + +| Pattern | Type | Example match | +| ------------------ | -------- | -------------------------- | +| `/users/profile` | Static | `/users/profile` | +| `/users/:id` | Param | `/users/42` | +| `/static/*` | Wildcard | `/static/css/app.css` | + +:::note +Priority is **static → param → wildcard**, so `/users/profile` always wins over +`/users/:id`, which wins over `/users/*`. +::: + +## Path parameters + +Read named parameters from the context with `c.Param()` (or `c.ParamOr()` for a default): + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +The wildcard segment is available as the `*` parameter: + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## Groups + +Group routes that share a prefix and middleware with `e.Group()`: + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +Groups can be nested to compose larger route trees. diff --git a/site/src/content/docs/guide/static-files.md b/site/src/content/docs/guide/static-files.md new file mode 100644 index 00000000..b2782be7 --- /dev/null +++ b/site/src/content/docs/guide/static-files.md @@ -0,0 +1,89 @@ +--- +title: Serving Static Files +description: Serve images, JavaScript, CSS, fonts, and other assets with Echo. +sidebar: + order: 9 +--- + +Echo can serve static assets such as images, JavaScript, CSS, PDFs, and fonts from +the filesystem or an embedded filesystem. + +## Default filesystem + +Echo uses `os.DirFS(".")` as its default filesystem, rooted at the current working +directory. To change it, set the `Echo#Filesystem` field: + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## Using the Static middleware + +See [Static middleware](/middleware/static/). + +## Using Echo#Static() + +`Echo#Static(prefix, root string)` registers a route that serves static files under +a path prefix from the given root directory. + +Serve any file from `assets` under `/static/*`. A request to `/static/js/main.js` +serves `assets/js/main.js`: + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +Serve any file from `assets` under `/*`. A request to `/js/main.js` serves +`assets/js/main.js`: + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## Using Echo#StaticFS() + +Static files can be served from any `fs.FS`, including an `embed.FS`. Use +`echo.MustSubFS` so the served files are rooted at the correct subdirectory — an +`embed.FS` includes its subdirectories as their own entries. + +```go +//go:embed "assets/images" +var images embed.FS + +func main() { + e := echo.New() + + e.StaticFS("/images", echo.MustSubFS(images, "assets/images")) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Using Echo#File() + +`Echo#File(path, file string)` registers a route that serves a single static file. + +Serve an index page from `public/index.html`: + +```go +e.File("/", "public/index.html") +``` + +Serve a favicon from `app/assets/favicon.ico`: + +```go +e := echo.New() +e.Filesystem = os.DirFS("/") +e.File("/favicon.ico", "app/assets/favicon.ico") // The file path must not have a leading slash. +``` + +:::caution +A leading `/` in the file path does not work with most `fs.FS` implementations. Use +a relative path. +::: diff --git a/site/src/content/docs/guide/templates.md b/site/src/content/docs/guide/templates.md new file mode 100644 index 00000000..4e43822c --- /dev/null +++ b/site/src/content/docs/guide/templates.md @@ -0,0 +1,133 @@ +--- +title: Templates +description: Render HTML templates with any engine by registering a renderer. +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` renders a template with data +and sends a `text/html` response with a status code. Register a renderer by setting +`Echo#Renderer`, which lets you use any template engine. + +## Rendering + +The example below uses Go's `html/template`. + +Use the default template renderer: + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +Or implement the `echo.Renderer` interface yourself: + +```go +type Template struct { + templates *template.Template +} + +func (t *Template) Render(c *echo.Context, w io.Writer, name string, data any) error { + return t.templates.ExecuteTemplate(w, name, data) +} +``` + +1. Pre-compile the templates. + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. Register the renderer. + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. Render a template inside the handler. + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## Advanced: calling Echo from templates + +Sometimes it is useful to generate URIs from within a template by calling +`Echo#Reverse`. Go's `html/template` is not ideally suited for this, but it can be +done in two ways: by providing a common method on every object passed to templates, +or by passing a `map[string]any` and augmenting it in the custom renderer. The +latter is more flexible. Here is a complete example. + +`template.html`: + +```html + + +

Hello {{index . "name"}}

+ +

{{ with $x := index . "reverse" }} + {{ call $x "foobar" }} + {{ end }} +

+ + +``` + +`server.go`: + +```go +package main + +import ( + "html/template" + "io" + "net/http" + + "github.com/labstack/echo/v5" +) + +// TemplateRenderer is a custom html/template renderer for Echo. +type TemplateRenderer struct { + templates *template.Template +} + +// Render renders a template document. +func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error { + // Add global methods if the data is a map. + if viewContext, isMap := data.(map[string]any); isMap { + viewContext["reverse"] = c.RouteInfo().Reverse + } + + return t.templates.ExecuteTemplate(w, name, data) +} + +func main() { + e := echo.New() + e.Renderer = &TemplateRenderer{ + templates: template.Must(template.ParseGlob("main/*.html")), + } + + e.GET("/something/:name", func(c *echo.Context) error { + return c.Render(http.StatusOK, "template.html", map[string]any{ + "name": "Dolly!", + }) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("shutting down the server", "error", err) + } +} +``` diff --git a/site/src/content/docs/guide/testing.md b/site/src/content/docs/guide/testing.md new file mode 100644 index 00000000..2efd3260 --- /dev/null +++ b/site/src/content/docs/guide/testing.md @@ -0,0 +1,264 @@ +--- +title: Testing +description: Test handlers and middleware with httptest and the echotest helpers. +sidebar: + order: 13 +--- + +Echo handlers and middleware are plain functions over an `echo.Context`, so they are +straightforward to test with the standard `net/http/httptest` package. The +`echotest` package provides helpers that cut down on boilerplate. + +## Testing a handler + +Consider two handlers: + +**CreateUser** — `POST /users` + +- Accepts a JSON payload. +- Returns `201 Created` on success. +- Returns `500 Internal Server Error` on error. + +**GetUser** — `GET /users/:email` + +- Returns `200 OK` on success. +- Returns `404 Not Found` if the user does not exist, otherwise `500 Internal Server Error`. + +`handler.go`: + +```go +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v5" +) + +type ( + User struct { + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + } + handler struct { + db map[string]*User + } +) + +func (h *handler) createUser(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + return c.JSON(http.StatusCreated, u) +} + +func (h *handler) getUser(c *echo.Context) error { + email := c.Param("email") + user := h.db[email] + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + return c.JSON(http.StatusOK, user) +} +``` + +`handler_test.go`: + +```go +package handler + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/echotest" + "github.com/stretchr/testify/assert" +) + +var ( + mockDB = map[string]*User{ + "jon@labstack.com": {Name: "Jon Snow", Email: "jon@labstack.com"}, + } + userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}` +) + +func TestCreateUser(t *testing.T) { + // Setup + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### Using the echotest helpers + +`echotest.ContextConfig` builds a context (and recorder) from a declarative +description of the request: + +```go +// Same test as above, using echotest. +func TestCreateUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} + +// Even shorter, using ServeWithHandler. +func TestCreateUserWithServeHandler(t *testing.T) { + h := &handler{mockDB} + + rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ServeWithHandler(t, h.createUser) + + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) +} + +func TestGetUser(t *testing.T) { + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }, + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.getUser(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### Using a form payload + +```go +// import "net/url" +f := make(url.Values) +f.Set("name", "Jon Snow") +f.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) +req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) +``` + +A multipart form payload with `echotest`: + +```go +func TestContext_MultipartForm(t *testing.T) { + testConf := echotest.ContextConfig{ + MultipartForm: &echotest.MultipartForm{ + Fields: map[string]string{ + "key": "value", + }, + Files: []echotest.MultipartFormFile{ + { + Fieldname: "file", + Filename: "test.json", + Content: echotest.LoadBytes(t, "testdata/test.json"), + }, + }, + }, + } + c := testConf.ToContext(t) + + assert.Equal(t, "value", c.FormValue("key")) + assert.Equal(t, http.MethodPost, c.Request().Method) + assert.Equal(t, true, strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "multipart/form-data; boundary=")) + + fv, err := c.FormFile("file") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "test.json", fv.Filename) +} +``` + +### Setting path parameters + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### Setting query parameters + +```go +// import "net/url" +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) +``` + +## Testing middleware + +```go +func TestMiddleware(t *testing.T) { + handler := func(c *echo.Context) error { + return c.JSON(http.StatusTeapot, fmt.Sprintf("email: %s", c.Param("email"))) + } + middleware := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("user_id", int64(1234)) + return next(c) + } + } + + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{{Name: "email", Value: "jon@labstack.com"}}, + }.ToContextRecorder(t) + + if err := middleware(handler)(c); err != nil { + t.Fatal(err) + } + + // Check that the middleware set the value. + userID, err := echo.ContextGet[int64](c, "user_id") + assert.NoError(t, err) + assert.Equal(t, int64(1234), userID) + + // Check that the handler returned the correct response. + assert.Equal(t, http.StatusTeapot, rec.Code) +} +``` + +:::tip +For more examples, see the [middleware test cases](https://github.com/labstack/echo/tree/master/middleware) +in the Echo source. +::: diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx new file mode 100644 index 00000000..cc9310ad --- /dev/null +++ b/site/src/content/docs/index.mdx @@ -0,0 +1,101 @@ +--- +title: Echo +description: High performance, extensible, minimalist Go web framework — a zero-allocation router, batteries-included middleware, and an expressive API. +template: splash +# Homepage gets a descriptive, keyword-rich instead of the bare +# "Echo | Echo" Starlight would derive from title === site title. +head: + - tag: title + content: Echo — High performance, minimalist Go web framework +# Minimal hero so Starlight folds the page title into the (hidden) hero region +# instead of rendering a separate title panel. Our real hero is <HomeHero/>. +hero: + tagline: ' ' +tableOfContents: false +--- + +import HomeHero from '../../components/HomeHero.astro'; +import { starsLabel } from '../../data/github.ts'; + +<HomeHero /> + +<div class="echo-stats"> + <div><b>{starsLabel}</b><span>GitHub stars</span></div> + <div><b>0 allocs</b><span>router, per request</span></div> + <div><b>25+</b><span>built-in middlewares</span></div> + <div><b>MIT</b><span>open source license</span></div> +</div> + +<div class="echo-h"><span class="ey">Why Echo</span><h2>Everything you need. Nothing you don't.</h2></div> + +<div class="echo-features"> + <div class="echo-card"><i class="ph ph-lightning"></i><h3>Optimized Router</h3><p>Radix-tree routing with zero dynamic allocation and smart route prioritization.</p></div> + <div class="echo-card"><i class="ph ph-stack"></i><h3>Batteries-included Middleware</h3><p>CORS, JWT, rate-limit, gzip, recover, request logging — 25+ built in.</p></div> + <div class="echo-card"><i class="ph ph-link"></i><h3>Data Binding</h3><p>Bind JSON, XML, form, query & path params into typed structs, with validation.</p></div> + <div class="echo-card"><i class="ph ph-lock-key"></i><h3>Automatic TLS</h3><p>HTTPS out of the box via Let's Encrypt, plus HTTP/2 support.</p></div> + <div class="echo-card"><i class="ph ph-puzzle-piece"></i><h3>Extensible</h3><p>Composable middleware and a clean, minimal interface for total control.</p></div> + <div class="echo-card"><i class="ph ph-code"></i><h3>Templates</h3><p>Plug in any Go template engine for fast, flexible HTML rendering.</p></div> +</div> + +<div class="echo-h"><span class="ey">Get Started</span><h2>A running server in three steps.</h2></div> + +<div class="echo-steps"> + <div class="echo-step"><span class="n">01</span><h4>Install</h4><p>Add Echo to your module.</p><pre>go get github.com/labstack/echo/v5</pre></div> + <div class="echo-step"><span class="n">02</span><h4>Write</h4><p>Register a route.</p><pre>e := echo.New() +e.GET("/", hello) +e.Start(":1323")</pre></div> + <div class="echo-step"><span class="n">03</span><h4>Run</h4><p>Start serving.</p><pre>go run main.go +⇨ :1323</pre></div> +</div> + +<div class="echo-h"><span class="ey">Ecosystem</span><h2>Official packages, ready to plug in.</h2></div> + +<div class="echo-eco"> + <a class="eco-card" href="https://github.com/labstack/echo-jwt" target="_blank" rel="noopener noreferrer"><h4>echo-jwt</h4><p>JWT authentication middleware backed by golang-jwt.</p></a> + <a class="eco-card" href="https://github.com/labstack/echo-contrib" target="_blank" rel="noopener noreferrer"><h4>echo-contrib</h4><p>Prometheus, Casbin, Jaeger, pprof, Zipkin & session helpers.</p></a> + <a class="eco-card" href="https://github.com/swaggo/echo-swagger" target="_blank" rel="noopener noreferrer"><h4>echo-swagger</h4><p>Generate and serve interactive Swagger / OpenAPI docs.</p></a> + <a class="eco-card" href="https://pkg.go.dev/github.com/labstack/echo/v5" target="_blank" rel="noopener noreferrer"><h4>API Reference</h4><p>Full package documentation on pkg.go.dev.</p></a> +</div> + +<div class="echo-h"><span class="ey">Sponsors</span><h2>Backed by teams who build on Echo.</h2></div> + +<a class="sp-featured" href="https://encore.dev" target="_blank" rel="noopener noreferrer"> + <img src="https://github.com/encoredev.png?size=88" alt="Encore" /> + <span class="t"><b>Encore</b><span>The platform for building Go-based cloud backends.</span></span> +</a> + +<div class="sp-grid"> + <a class="sp-card" href="https://github.com/customerio" target="_blank" rel="noopener noreferrer"><img src="https://github.com/customerio.png?size=52" alt="Customer.io" /><span>Customer.io</span></a> + <a class="sp-card" href="https://github.com/gravitycarbon" target="_blank" rel="noopener noreferrer"><img src="https://github.com/gravitycarbon.png?size=52" alt="Gravity Carbon" /><span>Gravity Carbon</span></a> + <a class="sp-card" href="https://github.com/mjslabs" target="_blank" rel="noopener noreferrer"><img src="https://github.com/mjslabs.png?size=52" alt="MJS Labs" /><span>MJS Labs</span></a> + <a class="sp-card" href="https://github.com/Codeerror" target="_blank" rel="noopener noreferrer"><img src="https://github.com/Codeerror.png?size=52" alt="Codeerror" /><span>Codeerror</span></a> +</div> + +<div class="sp-cta"><a href="https://github.com/sponsors/labstack" target="_blank" rel="noopener noreferrer"><i class="ph ph-heart"></i> Become a sponsor</a></div> + +<div class="echo-foot"> + <div class="brand"> + <span class="n">echo</span> + <p>High performance, extensible, minimalist Go web framework.</p> + </div> + <div class="col"> + <h4>Docs</h4> + <a href="/guide/quickstart/">Quickstart</a> + <a href="/guide/routing/">Routing</a> + <a href="/middleware/cors/">Middleware</a> + <a href="/cookbook/hello-world/">Cookbook</a> + </div> + <div class="col"> + <h4>Project</h4> + <a href="https://github.com/labstack/echo" target="_blank" rel="noopener noreferrer">GitHub</a> + <a href="https://github.com/labstack/echo/releases" target="_blank" rel="noopener noreferrer">Releases</a> + <a href="https://github.com/labstack/echo/discussions" target="_blank" rel="noopener noreferrer">Discussions</a> + <a href="https://github.com/labstack/echo/blob/master/.github/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">Contributing</a> + </div> + <div class="col"> + <h4>Resources</h4> + <a href="https://pkg.go.dev/github.com/labstack/echo/v5" target="_blank" rel="noopener noreferrer">API Reference</a> + <a href="https://github.com/sponsors/labstack" target="_blank" rel="noopener noreferrer">Sponsor</a> + <a href="https://github.com/labstack/echo/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">MIT License</a> + </div> +</div> diff --git a/site/src/content/docs/middleware/basic-auth.md b/site/src/content/docs/middleware/basic-auth.md new file mode 100644 index 00000000..cd93eebc --- /dev/null +++ b/site/src/content/docs/middleware/basic-auth.md @@ -0,0 +1,74 @@ +--- +title: Basic Auth +description: HTTP Basic authentication middleware that validates username and password credentials. +sidebar: + order: 1 +--- + +Basic Auth middleware provides HTTP basic authentication. + +- For valid credentials it calls the next handler. +- For missing or invalid credentials, it sends a `401 Unauthorized` response. + +## Usage + +```go +e.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (bool, error) { + // Use a constant time comparison to prevent timing attacks. + if subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 { + return true, nil + } + return false, nil +})) +``` + +## Custom configuration + +```go +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) +``` + +## Configuration + +```go +type BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator validates the credentials. If the request contains multiple basic + // auth headers, it is called once for each header until the first valid result. + // Required. + Validator BasicAuthValidator + + // Realm is the realm attribute of the WWW-Authenticate header. + // Default value "Restricted". + Realm string + + // AllowedCheckLimit sets how many headers are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + // Default value 1. + AllowedCheckLimit uint +} +``` + +The `Validator` has the signature: + +```go +type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error) +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset. +BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: "Restricted", +} +``` + +:::caution[Security] +Always compare credentials with `subtle.ConstantTimeCompare` to prevent timing attacks. +::: diff --git a/site/src/content/docs/middleware/body-dump.md b/site/src/content/docs/middleware/body-dump.md new file mode 100644 index 00000000..129b2ccf --- /dev/null +++ b/site/src/content/docs/middleware/body-dump.md @@ -0,0 +1,72 @@ +--- +title: Body Dump +description: Capture request and response payloads and pass them to a handler for logging or debugging. +sidebar: + order: 2 +--- + +Body Dump middleware captures the request and response payloads and passes them to a +registered handler. It is generally used for debugging or logging. + +:::caution +Avoid Body Dump for large payloads such as file uploads or downloads. If you must use it +on such routes, add an exception in the skipper function. +::: + +## Usage + +```go +e := echo.New() +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) { + // Handle the request and response bodies. +})) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) +``` + +## Configuration + +```go +type BodyDumpConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Handler receives the request and response payloads and the handler error, if any. + // Required. + Handler BodyDumpHandler + + // MaxRequestBytes limits how much of the request body to dump. If the request body + // exceeds this limit, only the first MaxRequestBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxRequestBytes int64 + + // MaxResponseBytes limits how much of the response body to dump. If the response body + // exceeds this limit, only the first MaxResponseBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxResponseBytes int64 +} +``` + +The `Handler` has the signature: + +```go +type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error) +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset (Handler is required). +BodyDumpConfig{ + Skipper: DefaultSkipper, + MaxRequestBytes: 5 * MB, + MaxResponseBytes: 5 * MB, +} +``` diff --git a/site/src/content/docs/middleware/body-limit.md b/site/src/content/docs/middleware/body-limit.md new file mode 100644 index 00000000..93eac3d0 --- /dev/null +++ b/site/src/content/docs/middleware/body-limit.md @@ -0,0 +1,48 @@ +--- +title: Body Limit +description: Reject requests whose body exceeds a configured maximum size. +sidebar: + order: 3 +--- + +Body Limit middleware sets the maximum allowed size for a request body. If the size +exceeds the configured limit, it sends a `413 Request Entity Too Large` response. + +The limit is enforced against both the `Content-Length` request header and the actual +content read, which makes it resilient against spoofed headers. The limit is specified +in bytes. + +## Usage + +```go +e := echo.New() +e.Use(middleware.BodyLimit(2_097_152)) // 2 MB +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) +``` + +## Configuration + +```go +type BodyLimitConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // LimitBytes is the maximum allowed size in bytes for a request body. + LimitBytes int64 +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset (Limit is required). +BodyLimitConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/middleware/casbin-auth.md b/site/src/content/docs/middleware/casbin-auth.md new file mode 100644 index 00000000..7012b66b --- /dev/null +++ b/site/src/content/docs/middleware/casbin-auth.md @@ -0,0 +1,203 @@ +--- +title: Casbin Auth +description: Authorize requests with the Casbin access control library using a small custom middleware. +sidebar: + order: 4 +--- + +[Casbin](https://github.com/casbin/casbin) is a powerful, efficient open-source access +control library for Go. It supports enforcing authorization across many models: + +- ACL (Access Control List) +- ACL with superuser +- ACL without users — useful for systems without authentication or user log-ins +- ACL without resources — target a type of resource (for example `write-article`, `read-log`) rather than an individual one +- RBAC (Role-Based Access Control) +- RBAC with resource roles — both users and resources can have roles +- RBAC with domains/tenants — users can have different role sets per domain/tenant +- ABAC (Attribute-Based Access Control) +- RESTful +- Deny-override — both allow and deny rules are supported, deny overrides allow + +See the [API overview](https://casbin.org/docs/api-overview) and the +[Casbin documentation](https://casbin.org/docs/) for details. + +## Dependencies + +```bash +go get github.com/casbin/casbin/v3 +``` + +```go +import ( + "github.com/casbin/casbin/v3" +) +``` + +## Implementation + +Echo does not ship a Casbin middleware; the integration is a small wrapper around the +Casbin enforcer: + +```go +// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/). +func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + username, err := userGetter(c) + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { + return echo.ErrInternalServerError.Wrap(err) + } else if !pass { + return echo.NewHTTPError(http.StatusForbidden, "access denied") + } + return next(c) + } + } +} +``` + +## Example + +Create a Casbin model file `auth_model.conf`: + +```ini +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") +``` + +Create a Casbin policy file `auth_policy.csv`: + +```csv +p, 1234567890, /dataset1/*, GET +p, alice, /dataset1/*, GET +p, alice, /dataset1/resource1, POST +p, bob, /dataset2/resource1, * +p, bob, /dataset2/resource2, GET +p, bob, /dataset2/folder1/*, POST +p, dataset1_admin, /dataset1/*, * +g, cathy, dataset1_admin +``` + +Authentication and authorization are separate concerns. Authenticate the user with +another middleware (such as JWT or Basic Auth), then supply a `userGetter` so Casbin +can authorize the request. + +### With JWT + +```go +e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication +jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return "", err + } + return token.Claims.GetSubject() +} +e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization +``` + +Try it with: + +```bash +curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" +``` + +### With Basic Auth + +```go +// BasicAuth middleware does authentication +e.Use(middleware.BasicAuth(func(c *echo.Context, user, password string) (bool, error) { + return subtle.ConstantTimeCompare([]byte(user), []byte("alice")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("password")) == 1, nil +})) +basicAuthUser := func(c *echo.Context) (string, error) { // Basic auth user getter for Casbin authorization + username, _, _ := c.Request().BasicAuth() // password is verified by the BasicAuth middleware above + return username, nil +} +e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization +``` + +Try it with: + +```bash +# should pass +curl -v -u "alice:password" http://localhost:8080/dataset1/any +# should fail +curl -v -u "alice:password" http://localhost:8080/dataset2/resource2 +``` + +### Full Casbin + JWT example + +```go +package main + +import ( + "log/slog" + "net/http" + + "github.com/casbin/casbin/v3" + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" +) + +// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/). +func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + username, err := userGetter(c) + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { + return echo.ErrInternalServerError.Wrap(err) + } else if !pass { + return echo.NewHTTPError(http.StatusForbidden, "access denied") + } + return next(c) + } + } +} + +func main() { + e := echo.New() + + ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") + if err != nil { + slog.Error("failed to initialize Casbin enforcer", "error", err) + } + + e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication + jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return "", err + } + return token.Claims.GetSubject() + } + e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization + + e.GET("/*", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/middleware/context-timeout.md b/site/src/content/docs/middleware/context-timeout.md new file mode 100644 index 00000000..99524e9f --- /dev/null +++ b/site/src/content/docs/middleware/context-timeout.md @@ -0,0 +1,47 @@ +--- +title: Context Timeout +description: Apply a timeout to the request context so context-aware operations can return early. +sidebar: + order: 5 +--- + +Context Timeout middleware applies a timeout to the request context within a predefined +period, so context-aware methods can return early once the deadline is exceeded. + +## Usage + +```go +e.Use(middleware.ContextTimeout(60 * time.Second)) +``` + +## Custom configuration + +```go +e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ + Timeout: 60 * time.Second, +})) +``` + +## Configuration + +```go +type ContextTimeoutConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // ErrorHandler is a function invoked when an error arises during middleware execution. + ErrorHandler func(c *echo.Context, err error) error + + // Timeout configures the timeout for the middleware. + Timeout time.Duration +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset (Timeout is required). +ContextTimeoutConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/middleware/cors.md b/site/src/content/docs/middleware/cors.md new file mode 100644 index 00000000..740ccfbe --- /dev/null +++ b/site/src/content/docs/middleware/cors.md @@ -0,0 +1,118 @@ +--- +title: CORS +description: Cross-Origin Resource Sharing middleware for secure cross-domain access control. +sidebar: + order: 6 +--- + +CORS middleware implements the [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol) specification. CORS gives +web servers cross-domain access controls, which enable secure cross-domain data transfers. + +## Usage + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, +})) +``` + +## Configuration + +```go +type CORSConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // AllowOrigins determines the value of the Access-Control-Allow-Origin response + // header, defining the list of origins that may access the resource. + // + // An origin consists of: scheme + "://" + host + optional ":" + port. + // A wildcard may be used, but it must be set explicitly as []string{"*"}. + // Example: `https://example.com`, `http://example.com:8080`, `*`. + // + // Security: use extreme caution when handling the origin and carefully validate any + // logic. Attackers may register hostile domain names. See + // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // Mandatory. + AllowOrigins []string + + // UnsafeAllowOriginFunc is an optional custom function to validate the origin. It + // takes the origin and returns the allowed origin, whether it is allowed, and an + // error (returned immediately by the handler). If set, AllowOrigins is ignored. + // + // Security: use extreme caution when handling the origin. Attackers may register + // hostile (sub)domain names. + // + // Sub-domain check example: + // UnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) { + // if strings.HasSuffix(origin, ".example.com") { + // return origin, true, nil + // } + // return "", false, nil + // } + // + // Optional. + UnsafeAllowOriginFunc func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error) + + // AllowMethods determines the value of the Access-Control-Allow-Methods response + // header, used in response to a preflight request. + // + // Optional. Defaults to GET, HEAD, PUT, PATCH, POST, DELETE. If left empty, the + // middleware fills the preflight Access-Control-Allow-Methods header from the + // `Allow` header that the router set into the context. + AllowMethods []string + + // AllowHeaders determines the value of the Access-Control-Allow-Headers response + // header, indicating which HTTP headers can be used in the actual request. + // + // Optional. Defaults to an empty list. + AllowHeaders []string + + // AllowCredentials determines the value of the Access-Control-Allow-Credentials + // response header, indicating whether the response can be exposed when the + // credentials mode is true. + // + // Optional. Default value false, in which case the header is not set. + // + // Security: avoid using AllowCredentials = true together with AllowOrigins = *. + AllowCredentials bool + + // ExposeHeaders determines the value of Access-Control-Expose-Headers, the list of + // headers clients are allowed to access. + // + // Optional. Default value []string{}, in which case the header is not set. + ExposeHeaders []string + + // MaxAge determines the value of the Access-Control-Max-Age response header, how long + // (in seconds) the results of a preflight request can be cached. The header is set + // only if MaxAge != 0; a negative value sends "0", instructing browsers not to cache. + // + // Optional. Default value 0 — the header is not sent. + MaxAge int +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset. +CORSConfig{ + Skipper: DefaultSkipper, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, +} +``` + +:::caution[Security] +Never combine `AllowCredentials = true` with a wildcard `AllowOrigins`. When you need +dynamic origin validation, use `UnsafeAllowOriginFunc` and validate carefully — +attackers may register hostile (sub)domain names. +::: diff --git a/site/src/content/docs/middleware/csrf.md b/site/src/content/docs/middleware/csrf.md new file mode 100644 index 00000000..0583c308 --- /dev/null +++ b/site/src/content/docs/middleware/csrf.md @@ -0,0 +1,177 @@ +--- +title: CSRF +description: Cross-Site Request Forgery protection using Sec-Fetch-Site metadata and token validation. +sidebar: + order: 7 +--- + +Cross-Site Request Forgery (CSRF, sometimes pronounced "sea-surf", or XSRF) is a type of +malicious exploit where unauthorized commands are transmitted from a user that a website +trusts. + +## Usage + +```go +e.Use(middleware.CSRF()) +``` + +## How it works + +The CSRF middleware supports the +[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) +header as a modern, defense-in-depth approach to +[CSRF protection](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers), +implementing the OWASP-recommended Fetch Metadata API alongside the traditional +token-based mechanism. + +Modern browsers automatically send the `Sec-Fetch-Site` header with every request, +indicating the relationship between the request origin and the target. The middleware +uses this to make a security decision: + +- **`same-origin`** or **`none`** — allowed (exact origin match or direct user navigation) +- **`same-site`** — falls back to token validation (for example, subdomain to main domain) +- **`cross-site`** — blocked by default with a `403` error for unsafe methods (POST, PUT, DELETE, PATCH) + +For browsers that do not send this header (older browsers), the middleware seamlessly +falls back to traditional token-based CSRF protection. + +Two options tune `Sec-Fetch-Site` behavior: + +- `TrustedOrigins []string` — allowlist specific origins for cross-site requests (useful for OAuth callbacks, webhooks) +- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)` — custom logic for same-site/cross-site validation + +```go +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + // Allow OAuth callbacks from a trusted provider. + TrustedOrigins: []string{"https://oauth-provider.com"}, + + // Custom validation for same-site/cross-site requests. + AllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) { + // Your custom authorization logic here. + return validateCustomAuth(c), nil + // return true, err // blocks the request with an error + // return true, nil // allows the request through + // return false, nil // falls back to legacy token logic + }, +})) +``` + +## Token-based protection + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +The example above extracts the CSRF token from the `X-XSRF-TOKEN` request header. + +Reading the token from a cookie instead: + +```go +middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "cookie:_csrf", + CookiePath: "/", + CookieDomain: "example.com", + CookieSecure: true, + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, +}) +``` + +## Accessing the CSRF token + +- **Server-side** — the token is available from the context under `ContextKey` and can be passed to the client via a template. +- **Client-side** — the token can be read from the CSRF cookie. + +## Configuration + +```go +type CSRFConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // TrustedOrigins permits any request with a `Sec-Fetch-Site` header whose `Origin` + // header exactly matches one of the listed values. Values should be formatted as + // the Origin header: "scheme://host[:port]". + TrustedOrigins []string + + // AllowSecFetchSiteFunc allows custom behaviour for `Sec-Fetch-Site` requests that + // are about to fail with a CSRF error, to be allowed or replaced with a custom + // error. Applies to `same-site` and `cross-site` values. + AllowSecFetchSiteFunc func(c *echo.Context) (bool, error) + + // TokenLength is the length of the generated token. + // Optional. Default value 32. + TokenLength uint8 + + // TokenLookup is a string in the form "<source>:<name>" or + // "<source>:<name>,<source>:<name>" used to extract the token from the request. + // Optional. Default value "header:X-CSRF-Token". + // Possible values: + // - "header:<name>" or "header:<name>:<cut-prefix>" + // - "query:<name>" + // - "form:<name>" + // Multiple sources example: "header:X-CSRF-Token,query:csrf". + TokenLookup string `yaml:"token_lookup"` + + // Generator defines a function to generate the token. + // Optional. Defaults to randomString(TokenLength). + Generator func() string + + // ContextKey is the key under which the generated CSRF token is stored in the context. + // Optional. Default value "csrf". + ContextKey string + + // CookieName is the name of the CSRF cookie that stores the token. + // Optional. Default value "_csrf". + CookieName string + + // CookieDomain is the domain of the CSRF cookie. + // Optional. Default value none. + CookieDomain string + + // CookiePath is the path of the CSRF cookie. + // Optional. Default value none. + CookiePath string + + // CookieMaxAge is the max age (in seconds) of the CSRF cookie. + // Optional. Default value 86400 (24h). + CookieMaxAge int + + // CookieSecure indicates whether the CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool + + // CookieHTTPOnly indicates whether the CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // CookieSameSite indicates the SameSite mode of the CSRF cookie. + // Optional. Default value SameSiteDefaultMode. + CookieSameSite http.SameSite + + // ErrorHandler defines a function that returns custom errors. + ErrorHandler func(c *echo.Context, err error) error +} +``` + +### Default configuration + +```go +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, +} +``` + +## Full example + +A complete, runnable example is available in the +[echox cookbook](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go). diff --git a/site/src/content/docs/middleware/decompress.md b/site/src/content/docs/middleware/decompress.md new file mode 100644 index 00000000..4112fd09 --- /dev/null +++ b/site/src/content/docs/middleware/decompress.md @@ -0,0 +1,59 @@ +--- +title: Decompress +description: Transparently decompress gzip-encoded request bodies. +sidebar: + order: 8 +--- + +Decompress middleware decompresses the HTTP request body when the `Content-Encoding` +header is set to `gzip`. + +:::note +The body is decompressed in memory and held there for the lifetime of the request (and +until garbage collection). +::: + +## Usage + +```go +e.Use(middleware.Decompress()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ + Skipper: middleware.DefaultSkipper, +})) +``` + +## Configuration + +```go +type DecompressConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // GzipDecompressPool provides the sync.Pool used to create and store gzip readers. + GzipDecompressPool Decompressor + + // MaxDecompressedSize limits the maximum size of the decompressed request body in + // bytes. If the decompressed body exceeds this limit, the middleware returns an + // HTTP 413 error. This prevents zip-bomb attacks where a small compressed payload + // decompresses to a huge size. + // Default: 100 * MB (104,857,600 bytes). Set to -1 to disable limits (not recommended). + MaxDecompressedSize int64 +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset. +DecompressConfig{ + Skipper: DefaultSkipper, + GzipDecompressPool: &DefaultGzipDecompressPool{}, + MaxDecompressedSize: 100 * MB, +} +``` diff --git a/site/src/content/docs/middleware/gzip.md b/site/src/content/docs/middleware/gzip.md new file mode 100644 index 00000000..5af5520a --- /dev/null +++ b/site/src/content/docs/middleware/gzip.md @@ -0,0 +1,69 @@ +--- +title: Gzip +description: Compress HTTP responses with the gzip compression scheme. +sidebar: + order: 9 +--- + +Gzip middleware compresses the HTTP response using the gzip compression scheme. + +## Usage + +```go +e.Use(middleware.Gzip()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +:::tip +Pass a skipper to disable gzip for certain URLs. +::: + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Skipper: func(c *echo.Context) bool { + return strings.Contains(c.Path(), "metrics") // change "metrics" to your own path + }, +})) +``` + +## Configuration + +```go +type GzipConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Level is the gzip compression level. + // Optional. Default value -1. + Level int + + // MinLength is the length threshold before gzip compression is applied. + // Optional. Default value 0. + // + // Most of the time the default is fine. Compressing a short response might increase + // the transmitted data because of gzip's format overhead, and compression consumes + // CPU and time on both server and client. Depending on your use case such a + // threshold can be useful. + MinLength int +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset. +GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + MinLength: 0, +} +``` diff --git a/site/src/content/docs/middleware/jwt.md b/site/src/content/docs/middleware/jwt.md new file mode 100644 index 00000000..de82fccc --- /dev/null +++ b/site/src/content/docs/middleware/jwt.md @@ -0,0 +1,126 @@ +--- +title: JWT +description: JSON Web Token authentication middleware provided by the echo-jwt module. +sidebar: + order: 10 +--- + +The JWT middleware provides JSON Web Token (JWT) authentication. It lives in a separate +module: [github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt). + +Behavior: + +- For a valid token, it sets the user in the context and calls the next handler. +- For an invalid token, it sends a `401 Unauthorized` response. +- For a missing or invalid `Authorization` header, it sends a `400 Bad Request` response. + +## Dependencies + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## Usage + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## Custom configuration + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## Configuration + +```go +type Config struct { + // Skipper defines a function to skip middleware. + Skipper middleware.Skipper + + // BeforeFunc defines a function which is executed just before the middleware. + BeforeFunc middleware.BeforeFunc + + // SuccessHandler defines a function executed for a valid token. If it returns an + // error, the middleware stops the handler chain and returns that error. + SuccessHandler func(c *echo.Context) error + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) + // or invalid key, and may be used to define a custom JWT error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users; the handler can set a default + // public JWT token value in the request and continue. + ErrorHandler func(c *echo.Context, err error) error + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool + + // ContextKey is the key under which user information from the token is stored in the context. + // Optional. Default value "user". + ContextKey string + + // SigningKey is the signing key used to validate the token. One of the three options + // to provide a token validation key. Order of precedence: user-defined KeyFunc, + // SigningKeys, then SigningKey. + // Required if neither a user-defined KeyFunc nor SigningKeys is provided. + SigningKey any + + // SigningKeys is a map of signing keys to validate tokens using the kid field. One of + // the three options to provide a token validation key. + // Required if neither a user-defined KeyFunc nor SigningKey is provided. + SigningKeys map[string]any + + // SigningMethod is the signing method used to check the token's signing algorithm. + // Not checked when a user-defined KeyFunc is provided. + // Optional. Default value HS256. + SigningMethod string + + // KeyFunc supplies the public key for token validation. It must verify the signing + // algorithm and select the proper key. Useful when tokens are issued by an external + // party. When provided, SigningKey, SigningKeys and SigningMethod are ignored. + // One of the three options to provide a token validation key, and not used if a + // custom ParseTokenFunc is set. + KeyFunc jwt.Keyfunc + + // TokenLookup is a string in the form "<source>:<name>" or + // "<source>:<name>,<source>:<name>" used to extract the token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:<name>" or "header:<name>:<cut-prefix>" + // <cut-prefix> trims a static prefix from the extracted value. For JWT tokens with + // `Authorization: Bearer <token>`, the prefix to cut is `Bearer ` (note the space). + // If the prefix is empty, the whole value is returned. + // - "query:<name>" + // - "param:<name>" + // - "cookie:<name>" + // - "form:<name>" + // Multiple sources example: "header:Authorization:Bearer ,cookie:myowncookie". + TokenLookup string + + // TokenLookupFuncs is a list of user-defined functions that extract the JWT token + // from the context. One of two options to provide a token extractor. Order of + // precedence: TokenLookupFuncs, then TokenLookup. Both may be provided. + TokenLookupFuncs []middleware.ValuesExtractor + + // ParseTokenFunc parses the token from the given auth string, returning an error when + // parsing fails or the token is invalid. + // Defaults to an implementation using github.com/golang-jwt/jwt. + ParseTokenFunc func(c *echo.Context, auth string) (any, error) + + // NewClaimsFunc returns the extendable claims defining token content. Used by the + // default ParseTokenFunc; not used if a custom ParseTokenFunc is set. + // Optional. Defaults to a function returning jwt.MapClaims. + NewClaimsFunc func(c *echo.Context) jwt.Claims +} +``` + +## Example + +See the [JWT cookbook](/cookbook/jwt/) for a complete example. diff --git a/site/src/content/docs/middleware/key-auth.md b/site/src/content/docs/middleware/key-auth.md new file mode 100644 index 00000000..be881c74 --- /dev/null +++ b/site/src/content/docs/middleware/key-auth.md @@ -0,0 +1,91 @@ +--- +title: Key Auth +description: Key-based authentication middleware that validates an API key from header, query, form, or cookie. +sidebar: + order: 11 +--- + +Key Auth middleware provides key-based authentication. + +- For a valid key it calls the next handler. +- For an invalid key, it sends a `401 Unauthorized` response. +- For a missing key, it sends a `400 Bad Request` response. + +## Usage + +```go +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil +})) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ + KeyLookup: "query:api-key", + Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil + }, +})) +``` + +## Configuration + +```go +type KeyAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // KeyLookup is a string in the form "<source>:<name>" or + // "<source>:<name>,<source>:<name>" used to extract the key from the request. + // Optional. Default value "header:Authorization:Bearer ". + // Possible values: + // - "header:<name>" or "header:<name>:<cut-prefix>" + // <cut-prefix> trims a static prefix from the extracted value. For + // `Authorization: Basic <credentials>`, the prefix to remove is `Basic `. + // - "query:<name>" + // - "form:<name>" + // - "cookie:<name>" + // Multiple sources example: "header:Authorization,header:X-Api-Key". + KeyLookup string + + // AllowedCheckLimit sets how many KeyLookup values are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + AllowedCheckLimit uint + + // Validator validates the key. + // Required. + Validator KeyAuthValidator + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) or + // invalid key, and may be used to define a custom error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users. + ErrorHandler KeyAuthErrorHandler + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool +} +``` + +The `Validator` has the signature: + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### Default configuration + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/middleware/logger.md b/site/src/content/docs/middleware/logger.md new file mode 100644 index 00000000..a3699705 --- /dev/null +++ b/site/src/content/docs/middleware/logger.md @@ -0,0 +1,236 @@ +--- +title: Request Logger +description: Fully customizable request logging that integrates with structured logging libraries. +sidebar: + order: 12 +--- + +`RequestLogger` middleware logs information about each HTTP request. It lets you fully +customize what is logged and how, making it well suited for use with third-party +(structured logging) libraries. + +The values the logger can extract are controlled by the boolean and slice fields of +`RequestLoggerConfig`. Enable a field (for example `LogStatus: true`) to have its value +populated on the `RequestLoggerValues` passed to your `LogValuesFunc`. + +```go +type RequestLoggerConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // BeforeNextFunc is called before the next middleware or handler in the chain. + BeforeNextFunc func(c *echo.Context) + + // LogValuesFunc is called with the values extracted by the logger from the + // request/response. + // Mandatory. + LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error + + // HandleError instructs the logger to call the global error handler when the next + // middleware/handler returns an error. A side effect is that the response is then + // committed and sent, so middlewares up the chain can no longer change the status + // code or body. + HandleError bool + + // LogLatency records the duration of the rest of the handler chain (the next(c) call). + LogLatency bool + // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). + LogProtocol bool + // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. + LogRemoteIP bool + // LogHost extracts the request host value (for example example.com). + LogHost bool + // LogMethod extracts the request method (for example GET). + LogMethod bool + // LogURI extracts the request URI (for example /list?lang=en&page=1). + LogURI bool + // LogURIPath extracts the request URI path part (for example /list). + LogURIPath bool + // LogRoutePath extracts the route path the request matched (for example /user/:id). + LogRoutePath bool + // LogRequestID extracts the request ID from the X-Request-ID request header, or the + // response if the request did not have a value. + LogRequestID bool + // LogReferer extracts the request referer value. + LogReferer bool + // LogUserAgent extracts the request user agent value. + LogUserAgent bool + // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, + // the status code is taken from it. + LogStatus bool + // LogError extracts the error returned from the handler chain. + LogError bool + // LogContentLength extracts the Content-Length header value. Note: this can differ + // from the actual request body size as it may be spoofed. + LogContentLength bool + // LogResponseSize extracts the response content length. Note: when used with Gzip + // middleware this value may not always be correct. + LogResponseSize bool + // LogHeaders extracts the given list of request headers. A slice of values is logged + // per header since a request can contain more than one. Names are canonicalized with + // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). + LogHeaders []string + // LogQueryParams extracts the given list of query parameters from the request URI. A + // slice of values is logged per name since a request can repeat a parameter. + LogQueryParams []string + // LogFormValues extracts the given list of form values from the request body and URI. + // A slice of values is logged per name since a request can repeat a value. + LogFormValues []string +} +``` + +## Examples + +### fmt.Printf + +```go +skipper := func(c *echo.Context) bool { + // Skip the health check endpoint. + return c.Request().URL.Path == "/health" +} +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + Skipper: skipper, + BeforeNextFunc: func(c *echo.Context) { + c.Set("customValueFromContext", 42) + }, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + value, _ := c.Get("customValueFromContext").(int) + fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) + return nil + }, +})) +``` + +Sample output: + +```text +REQUEST: uri: /hello, status: 200, custom-value: 42 +``` + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +```go +logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogError: true, + HandleError: true, // forwards the error to the global error handler so it can pick the status code + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + ) + } else { + logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.String("err", v.Error.Error()), + ) + } + return nil + }, +})) +``` + +Sample output: + +```text +{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} +``` + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +```go +logger := zerolog.New(os.Stdout) +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger.Info(). + Str("URI", v.URI). + Int("status", v.Status). + Msg("request") + return nil + }, +})) +``` + +Sample output: + +```text +{"level":"info","URI":"/hello","status":200,"message":"request"} +``` + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +```go +logger, _ := zap.NewProduction() +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger.Info("request", + zap.String("URI", v.URI), + zap.Int("status", v.Status), + ) + return nil + }, +})) +``` + +Sample output: + +```text +{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} +``` + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +```go +log := logrus.New() +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { + log.WithFields(logrus.Fields{ + "URI": values.URI, + "status": values.Status, + }).Info("request") + return nil + }, +})) +``` + +Sample output: + +```text +time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 +``` + +## Troubleshooting + +### panic: missing LogValuesFunc callback function for request logger middleware + +This panic occurs when the mandatory `LogValuesFunc` callback is left unset. Define a +function matching the `LogValuesFunc` signature and assign it in the configuration: + +```go +func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { + fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) + return nil +} + +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogValuesFunc: logValues, +})) +``` + +### Parameters in logs are empty + +If values such as `v.URI` and `v.Status` are empty inside `LogValuesFunc`, check that the +corresponding extraction flags (`LogStatus`, `LogURI`, and so on) are set to `true` in +the configuration. Each value is only populated when its flag is enabled. diff --git a/site/src/content/docs/middleware/method-override.md b/site/src/content/docs/middleware/method-override.md new file mode 100644 index 00000000..e4cb01d1 --- /dev/null +++ b/site/src/content/docs/middleware/method-override.md @@ -0,0 +1,52 @@ +--- +title: Method Override +description: Override the HTTP method of a POST request via header, form, or query value. +sidebar: + order: 13 +--- + +Method Override middleware reads the overridden method from the request and uses it +instead of the original method. + +:::note +For security reasons, only the `POST` method can be overridden. +::: + +## Usage + +```go +e.Pre(middleware.MethodOverride()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +The method can be sourced with `MethodFromHeader`, `MethodFromForm`, or `MethodFromQuery`. + +## Configuration + +```go +type MethodOverrideConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Getter is a function that gets the overridden method from the request. + // Optional. Default value MethodFromHeader(echo.HeaderXHTTPMethodOverride). + Getter MethodOverrideGetter +} +``` + +### Default configuration + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/site/src/content/docs/middleware/open-telemetry.md b/site/src/content/docs/middleware/open-telemetry.md new file mode 100644 index 00000000..1f97e2b2 --- /dev/null +++ b/site/src/content/docs/middleware/open-telemetry.md @@ -0,0 +1,81 @@ +--- +title: OpenTelemetry +description: OpenTelemetry instrumentation for HTTP requests in Echo. +sidebar: + order: 14 +--- + +[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) is a middleware that +provides OpenTelemetry instrumentation for HTTP requests. + +OpenTelemetry is a set of open-source tools that provide instrumentation for cloud-native +applications. + +- [OpenTelemetry Exporters](https://opentelemetry.io/docs/languages/go/exporters/) +- [OpenTelemetry HTTP spec](https://opentelemetry.io/docs/specs/semconv/http/) +- [HTTP metrics spec](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/) + +## Usage + +Add the OpenTelemetry middleware dependency with Go modules: + +```bash +go get github.com/labstack/echo-opentelemetry +``` + +Import the middleware and the OpenTelemetry trace API: + +```go +import ( + echootel "github.com/labstack/echo-opentelemetry" + "go.opentelemetry.io/otel/trace" +) +``` + +Register it with full configuration: + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + ServerName: "my-server", + TracerProvider: tp, + + //Skipper: nil, + //OnNextError: nil, + //OnExtractionError: nil, + //MeterProvider: nil, + //Propagators: nil, + //SpanStartOptions: nil, + //SpanStartAttributes: nil, + //SpanEndAttributes: nil, + //MetricAttributes: nil, + //Metrics: nil, +})) +``` + +For configuration options, see the [`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28) struct. + +Add the middleware in simplified form by providing only the server name: + +```go +e.Use(echootel.NewMiddleware("app.example.com")) +``` + +Add the middleware with configuration options: + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + TracerProvider: tp, +})) +``` + +Retrieve the tracer from the Echo context: + +```go +tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) +``` + +## Example + +The [example](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) exports +metrics and spans to stdout, but you can use any exporter (OTLP, etc.). See the +[OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters) documentation. diff --git a/site/src/content/docs/middleware/prometheus.md b/site/src/content/docs/middleware/prometheus.md new file mode 100644 index 00000000..3a70a999 --- /dev/null +++ b/site/src/content/docs/middleware/prometheus.md @@ -0,0 +1,284 @@ +--- +title: Prometheus +description: Generate Prometheus metrics for HTTP requests in Echo. +sidebar: + order: 15 +--- + +[Echo Prometheus](https://github.com/labstack/echo-prometheus) middleware generates Prometheus +metrics for HTTP requests. + +## Usage + +Add the required module: + +```bash +go get github.com/labstack/echo-prometheus +``` + +Add the Prometheus middleware and a route to serve the gathered metrics: + +```go +e := echo.New() +e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics +e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics +``` + +## Examples + +Serve metrics from the same server that gathers them: + +```go +package main + +import ( + "net/http" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() + + e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Serve metrics on a separate port: + +```go +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + + go func() { + metrics := echo.New() // this Echo will run on separate port 8081 + metrics.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + if err := metrics.Start(":8081"); err != nil { + e.Logger.Error("failed to start metrics server", "error", err) + } + }() + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Sample output (for the first example): + +```bash +curl http://localhost:8080/metrics + +# HELP echo_request_duration_seconds The HTTP request latencies in seconds. +# TYPE echo_request_duration_seconds summary +echo_request_duration_seconds_sum 0.41086482 +echo_request_duration_seconds_count 1 +# HELP echo_request_size_bytes The HTTP request sizes in bytes. +# TYPE echo_request_size_bytes summary +echo_request_size_bytes_sum 56 +echo_request_size_bytes_count 1 +# HELP echo_requests_total How many HTTP requests processed, partitioned by status code and HTTP method. +# TYPE echo_requests_total counter +echo_requests_total{code="200",host="localhost:8080",method="GET",url="/"} 1 +# HELP echo_response_size_bytes The HTTP response sizes in bytes. +# TYPE echo_response_size_bytes summary +echo_response_size_bytes_sum 61 +echo_response_size_bytes_count 1 +... +``` + +## Custom configuration + +### Serving custom Prometheus metrics + +Use custom metrics with the Prometheus default registry: + +```go +package main + +import ( + "log" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + customCounter := prometheus.NewCounter( // create a new counter metric + prometheus.CounterOpts{ + Name: "custom_requests_total", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", + }, + ) + if err := prometheus.Register(customCounter); err != nil { // register the counter with the default registry + log.Fatal(err) + } + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + AfterNext: func(c *echo.Context, err error) { + customCounter.Inc() // increment the counter after every request + }, + })) + e.GET("/metrics", echoprometheus.NewHandler()) // register a route to serve gathered metrics + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Or create your own registry and register custom metrics with it: + +```go +package main + +import ( + "log" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + customRegistry := prometheus.NewRegistry() // create a custom registry for your custom metrics + customCounter := prometheus.NewCounter( // create a new counter metric + prometheus.CounterOpts{ + Name: "custom_requests_total", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", + }, + ) + if err := customRegistry.Register(customCounter); err != nil { // register the counter with the custom registry + log.Fatal(err) + } + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + AfterNext: func(c *echo.Context, err error) { + customCounter.Inc() // increment the counter after every request + }, + Registerer: customRegistry, // use the custom registry instead of the default Prometheus registry + })) + e.GET("/metrics", echoprometheus.NewHandlerWithConfig(echoprometheus.HandlerConfig{Gatherer: customRegistry})) // serve metrics from the custom registry + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Skipping URLs + +A skipper can be passed to avoid generating metrics for certain URLs: + +```go +package main + +import ( + "net/http" + "strings" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + mwConfig := echoprometheus.MiddlewareConfig{ + Skipper: func(c *echo.Context) bool { + return strings.HasPrefix(c.Path(), "/testurl") + }, // does not gather metrics on routes starting with `/testurl` + } + e.Use(echoprometheus.NewMiddlewareWithConfig(mwConfig)) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Complex scenarios + +Modify the default `echoprometheus` metric definitions: + +```go +package main + +import ( + "net/http" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + // Labels of default metrics can be modified or added with the `LabelFuncs` function. + LabelFuncs: map[string]echoprometheus.LabelValueFunc{ + "scheme": func(c *echo.Context, err error) string { // additional custom label + return c.Scheme() + }, + "host": func(c *echo.Context, err error) string { // overrides the default 'host' label value + return "y_" + c.Request().Host + }, + }, + // The `echoprometheus` middleware registers the following metrics by default: + // - Histogram: request_duration_seconds + // - Histogram: response_size_bytes + // - Histogram: request_size_bytes + // - Counter: requests_total + // which can be modified with the `HistogramOptsFunc` and `CounterOptsFunc` functions. + HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { + if opts.Name == "request_duration_seconds" { + opts.Buckets = []float64{1000.0, 10_000.0, 100_000.0, 1_000_000.0} // 1KB, 10KB, 100KB, 1MB + } + return opts + }, + CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts { + if opts.Name == "requests_total" { + opts.ConstLabels = prometheus.Labels{"my_const": "123"} + } + return opts + }, + })) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/middleware/proxy.md b/site/src/content/docs/middleware/proxy.md new file mode 100644 index 00000000..f33c1756 --- /dev/null +++ b/site/src/content/docs/middleware/proxy.md @@ -0,0 +1,134 @@ +--- +title: Proxy +description: HTTP and WebSocket reverse proxy middleware with load balancing. +sidebar: + order: 16 +--- + +Proxy provides an HTTP/WebSocket reverse proxy middleware. It forwards a request to an +upstream server using a configured load balancing technique. + +## Usage + +```go +url1, err := url.Parse("http://localhost:8081") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +url2, err := url.Parse("http://localhost:8082") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ + { + URL: url1, + }, + { + URL: url2, + }, +}))) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## Configuration + +```go +type ProxyConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Balancer defines a load balancing technique. + // Required. + Balancer ProxyBalancer + + // RetryCount defines the number of times a failed proxied request should be retried + // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried. + RetryCount int + + // RetryFilter defines a function used to determine if a failed request to a + // ProxyTarget should be retried. The RetryFilter will only be called when the number + // of previous retries is less than RetryCount. If the function returns true, the + // request will be retried. The provided error indicates the reason for the request + // failure. When the ProxyTarget is unavailable, the error will be an instance of + // echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error + // will indicate an internal error in the Proxy middleware. When a RetryFilter is not + // specified, all requests that fail with http.StatusBadGateway will be retried. A custom + // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is + // only called when the request to the target fails, or an internal error in the Proxy + // middleware has occurred. Successful requests that return a non-200 response code cannot + // be retried. + RetryFilter func(c *echo.Context, e error) bool + + // ErrorHandler defines a function which can be used to return custom errors from + // the Proxy middleware. ErrorHandler is only invoked when there has been + // either an internal error in the Proxy middleware or the ProxyTarget is + // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked + // when a ProxyTarget returns a non-200 response. In these cases, the response + // is already written so errors cannot be modified. ErrorHandler is only + // invoked after all retry attempts have been exhausted. + ErrorHandler func(c *echo.Context, err error) error + + // Rewrite defines URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Examples: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rewrite map[string]string + + // RegexRewrite defines rewrite rules using regexp.Regexp with captures. + // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. + // Example: + // "^/old/[0.9]+/": "/new", + // "^/api/.+?/(.*)": "/v2/$1", + RegexRewrite map[*regexp.Regexp]string + + // Context key to store selected ProxyTarget into context. + // Optional. Default value "target". + ContextKey string + + // To customize the transport to remote. + // Examples: If custom TLS certificates are required. + Transport http.RoundTripper + + // ModifyResponse defines function to modify response from ProxyTarget. + ModifyResponse func(*http.Response) error +} +``` + +### Default configuration + +| Name | Value | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### Regex-based rules + +For advanced rewriting of proxy requests, rules may also be defined using regular +expressions. Normal capture groups can be defined using `()` and referenced by index +(`$1`, `$2`, ...) in the rewritten path. + +`RegexRewrite` and normal `Rewrite` rules can be combined. + +```go +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ + Balancer: rrb, + Rewrite: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRewrite: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` + +See the [reverse proxy](/cookbook/reverse-proxy/) cookbook for a complete example. diff --git a/site/src/content/docs/middleware/rate-limiter.md b/site/src/content/docs/middleware/rate-limiter.md new file mode 100644 index 00000000..0c8dd335 --- /dev/null +++ b/site/src/content/docs/middleware/rate-limiter.md @@ -0,0 +1,109 @@ +--- +title: Rate Limiter +description: Limit the number of requests from a particular IP or identifier within a time period. +sidebar: + order: 17 +--- + +`RateLimiter` provides a rate limiter middleware that limits the number of requests sent to +the server from a particular IP or identifier within a time period. + +By default, an in-memory store keeps track of requests. The default in-memory implementation +is focused on correctness and may not be the best option for a high number of concurrent +requests or a large number of distinct identifiers (>16k). + +## Usage + +To add a rate limit to your application, add the `RateLimiter` middleware. The example below +limits the application to 20 requests/sec using the default in-memory store: + +```go +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) +``` + +:::note +If the provided rate is a float number, `Burst` is treated as the rounded-down value of the rate. +::: + +## Custom configuration + +```go +config := middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}, + ), + IdentifierExtractor: func(c *echo.Context) (string, error) { + id := c.RealIP() + return id, nil + }, + ErrorHandler: func(c *echo.Context, err error) error { + return c.JSON(http.StatusForbidden, nil) + }, + DenyHandler: func(c *echo.Context, identifier string, err error) error { + return c.JSON(http.StatusTooManyRequests, nil) + }, +} + +e.Use(middleware.RateLimiterWithConfig(config)) +``` + +### Errors + +```go +var ( + // ErrRateLimitExceeded denotes an error raised when the rate limit is exceeded. + ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") + // ErrExtractorError denotes an error raised when the extractor function is unsuccessful. + ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier") +) +``` + +:::tip +To implement your own store, satisfy the `RateLimiterStore` interface and pass it to +`RateLimiterConfig`. +::: + +## Configuration + +```go +type RateLimiterConfig struct { + Skipper Skipper + BeforeFunc BeforeFunc + // IdentifierExtractor uses echo.Context to extract the identifier for a visitor. + IdentifierExtractor Extractor + // Store defines a store for the rate limiter. + Store RateLimiterStore + // ErrorHandler provides a handler to be called when IdentifierExtractor returns a non-nil error. + ErrorHandler func(c *echo.Context, err error) error + // DenyHandler provides a handler to be called when RateLimiter denies access. + DenyHandler func(c *echo.Context, identifier string, err error) error +} +``` + +### Default configuration + +```go +// DefaultRateLimiterConfig defines default values for RateLimiterConfig. +var DefaultRateLimiterConfig = RateLimiterConfig{ + Skipper: DefaultSkipper, + IdentifierExtractor: func(c *echo.Context) (string, error) { + id := c.RealIP() + return id, nil + }, + ErrorHandler: func(c *echo.Context, err error) error { + return &echo.HTTPError{ + Code: ErrExtractorError.Code, + Message: ErrExtractorError.Message, + Internal: err, + } + }, + DenyHandler: func(c *echo.Context, identifier string, err error) error { + return &echo.HTTPError{ + Code: ErrRateLimitExceeded.Code, + Message: ErrRateLimitExceeded.Message, + Internal: err, + } + }, +} +``` diff --git a/site/src/content/docs/middleware/recover.md b/site/src/content/docs/middleware/recover.md new file mode 100644 index 00000000..0146171e --- /dev/null +++ b/site/src/content/docs/middleware/recover.md @@ -0,0 +1,61 @@ +--- +title: Recover +description: Recover from panics anywhere in the chain and delegate to the centralized error handler. +sidebar: + order: 18 +--- + +Recover middleware recovers from panics anywhere in the chain, prints the stack trace, and +passes control to the centralized +[HTTPErrorHandler](/guide/customization/#http-error-handler). + +## Usage + +```go +e.Use(middleware.Recover()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +The example above uses a `StackSize` of 1 KB and default values for `DisableStackAll` and +`DisablePrintStack`. + +## Configuration + +```go +type RecoverConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Size of the stack to be printed. + // Optional. Default value 4KB. + StackSize int + + // DisableStackAll disables formatting stack traces of all other goroutines + // into the buffer after the trace for the current goroutine. + // Optional. Default value false. + DisableStackAll bool + + // DisablePrintStack disables printing the stack trace. + // Optional. Default value false. + DisablePrintStack bool +} +``` + +### Default configuration + +```go +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/site/src/content/docs/middleware/redirect.md b/site/src/content/docs/middleware/redirect.md new file mode 100644 index 00000000..b071f0ec --- /dev/null +++ b/site/src/content/docs/middleware/redirect.md @@ -0,0 +1,101 @@ +--- +title: Redirect +description: Redirect requests between HTTP/HTTPS and www/non-www variants. +sidebar: + order: 19 +--- + +## HTTPS Redirect + +HTTPS redirect middleware redirects HTTP requests to HTTPS. For example, +`http://labstack.com` is redirected to `https://labstack.com`. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPS WWW Redirect + +HTTPS WWW redirect redirects HTTP requests to www HTTPS. For example, +`http://labstack.com` is redirected to `https://www.labstack.com`. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPS NonWWW Redirect + +HTTPS NonWWW redirect redirects HTTP requests to non-www HTTPS. For example, +`http://www.labstack.com` is redirected to `https://labstack.com`. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.HTTPSNonWWWRedirect()) +``` + +## WWW Redirect + +WWW redirect redirects non-www requests to www. For example, `http://labstack.com` is +redirected to `http://www.labstack.com`. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWW Redirect + +NonWWW redirect redirects www requests to non-www. For example, `http://www.labstack.com` is +redirected to `http://labstack.com`. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +The example above redirects HTTP requests to HTTPS with status code +`307 - StatusTemporaryRedirect`. + +## Configuration + +```go +type RedirectConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional. Default value http.StatusMovedPermanently. + Code int +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset. +RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/site/src/content/docs/middleware/request-id.md b/site/src/content/docs/middleware/request-id.md new file mode 100644 index 00000000..692f85d9 --- /dev/null +++ b/site/src/content/docs/middleware/request-id.md @@ -0,0 +1,89 @@ +--- +title: Request ID +description: Generate a unique ID for each request. +sidebar: + order: 20 +--- + +Request ID middleware generates a unique ID for a request. + +## Usage + +```go +e.Use(middleware.RequestID()) +``` + +Example: + +```go +func main() { + e := echo.New() + + e.Use(middleware.RequestID()) + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID)) + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Custom configuration + +```go +e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ + Generator: func() string { + return customGenerator() + }, +})) +``` + +## Configuration + +```go +type RequestIDConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Generator defines a function to generate an ID. + // Optional. Default value random.String(32). + Generator func() string + + // RequestIDHandler defines a function which is executed for a request id. + RequestIDHandler func(c *echo.Context, requestID string) + + // TargetHeader defines what header to look for to populate the id. + // Optional. Default value is `X-Request-Id`. + TargetHeader string +} +``` + +### Default configuration + +```go +// Effective defaults applied when fields are left unset. +RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, // random 32-character string + TargetHeader: echo.HeaderXRequestID, +} +``` + +## Set ID + +You can set the ID from the requester with the `X-Request-ID` header. + +### Request + +```sh +curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param" +``` + +### Log + +```js +{"time":"2017-11-13T20:26:28.6438003+01:00","id":"3","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/?my=param","my":"param","status":200, "latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":13} +``` diff --git a/site/src/content/docs/middleware/rewrite.md b/site/src/content/docs/middleware/rewrite.md new file mode 100644 index 00000000..fd89bfaa --- /dev/null +++ b/site/src/content/docs/middleware/rewrite.md @@ -0,0 +1,87 @@ +--- +title: Rewrite +description: Rewrite the URL path based on configured rules. +sidebar: + order: 21 +--- + +Rewrite middleware rewrites the URL path based on the provided rules. It is helpful for +backward compatibility or for creating cleaner and more descriptive links. + +## Usage + +```go +e.Pre(middleware.Rewrite(map[string]string{ + "/old": "/new", + "/api/*": "/$1", + "/js/*": "/public/javascripts/$1", + "/users/*/orders/*": "/user/$1/order/$2", +})) +``` + +The values captured in asterisks can be retrieved by index, e.g. `$1`, `$2`, and so on. Each +asterisk is non-greedy (translated to a capture group `(.*?)`); when using multiple asterisks, +a trailing `*` matches the rest of the path. + +:::caution +Rewrite middleware should be registered via `Echo#Pre()` so it runs before the router. +::: + +## Custom configuration + +```go +e := echo.New() +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) +``` + +## Configuration + +```go +type RewriteConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + // Required. + Rules map[string]string + + // RegexRules defines the URL path rewrite rules using regexp.Regexp with captures. + // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. + // Example: + // "^/old/[0.9]+/": "/new", + // "^/api/.+?/(.*)": "/v2/$1", + RegexRules map[*regexp.Regexp]string +} +``` + +Default configuration: + +| Name | Value | +| ------- | -------------- | +| Skipper | DefaultSkipper | + +### Regex-based rules + +For advanced rewriting of paths, rules may also be defined using regular expressions. Normal +capture groups can be defined using `()` and referenced by index (`$1`, `$2`, ...) in the +rewritten path. + +`RegexRules` and normal `Rules` can be combined. + +```go +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{ + Rules: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRules: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` diff --git a/site/src/content/docs/middleware/secure.md b/site/src/content/docs/middleware/secure.md new file mode 100644 index 00000000..d7a3d3b5 --- /dev/null +++ b/site/src/content/docs/middleware/secure.md @@ -0,0 +1,113 @@ +--- +title: Secure +description: Protect against XSS, content sniffing, clickjacking, and other injection attacks. +sidebar: + order: 22 +--- + +Secure middleware provides protection against cross-site scripting (XSS), content type +sniffing, clickjacking, insecure connections, and other code injection attacks. + +## Usage + +```go +e.Use(middleware.Secure()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +:::note +Passing an empty `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions`, or +`ContentSecurityPolicy` disables that protection. +::: + +## Configuration + +```go +type SecureConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // XSSProtection provides protection against cross-site scripting attack (XSS) + // by setting the `X-XSS-Protection` header. + // Optional. Default value "1; mode=block". + XSSProtection string + + // ContentTypeNosniff provides protection against overriding Content-Type + // header by setting the `X-Content-Type-Options` header. + // Optional. Default value "nosniff". + ContentTypeNosniff string + + // XFrameOptions can be used to indicate whether or not a browser should + // be allowed to render a page in a <frame>, <iframe> or <object>. + // Sites can use this to avoid clickjacking attacks, by ensuring that their + // content is not embedded into other sites. + // Optional. Default value "SAMEORIGIN". + // Possible values: + // - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself. + // - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so. + // - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin. + XFrameOptions string + + // HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how + // long (in seconds) browsers should remember that this site is only to + // be accessed using HTTPS. This reduces your exposure to some SSL-stripping + // man-in-the-middle (MITM) attacks. + // Optional. Default value 0. + HSTSMaxAge int + + // HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security` + // header, excluding all subdomains from security policy. It has no effect + // unless HSTSMaxAge is set to a non-zero value. + // Optional. Default value false. + HSTSExcludeSubdomains bool + + // ContentSecurityPolicy sets the `Content-Security-Policy` header providing + // security against cross-site scripting (XSS), clickjacking and other code + // injection attacks resulting from execution of malicious content in the + // trusted web page context. + // Optional. Default value "". + ContentSecurityPolicy string + + // CSPReportOnly would use the `Content-Security-Policy-Report-Only` header instead + // of the `Content-Security-Policy` header. This allows iterative updates of the + // content security policy by only reporting the violations that would + // have occurred instead of blocking the resource. + // Optional. Default value false. + CSPReportOnly bool + + // HSTSPreloadEnabled will add the preload tag in the `Strict Transport Security` + // header, which enables the domain to be included in the HSTS preload list + // maintained by Chrome (and used by Firefox and Safari): https://hstspreload.org/ + // Optional. Default value false. + HSTSPreloadEnabled bool + + // ReferrerPolicy sets the `Referrer-Policy` header providing security against + // leaking potentially sensitive request paths to third parties. + // Optional. Default value "". + ReferrerPolicy string +} +``` + +### Default configuration + +```go +var DefaultSecureConfig = SecureConfig{ + Skipper: DefaultSkipper, + XSSProtection: "1; mode=block", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSPreloadEnabled: false, +} +``` diff --git a/site/src/content/docs/middleware/session.md b/site/src/content/docs/middleware/session.md new file mode 100644 index 00000000..13451bac --- /dev/null +++ b/site/src/content/docs/middleware/session.md @@ -0,0 +1,189 @@ +--- +title: Session +description: HTTP session management backed by gorilla/sessions. +sidebar: + order: 23 +--- + +Session middleware facilitates HTTP session management backed by +[gorilla/sessions](https://github.com/gorilla/sessions). The default implementation provides +cookie- and filesystem-based session stores; you can also use a +[community-maintained store](https://github.com/gorilla/sessions#store-implementations) for +various backends. + +## Dependencies + +```bash +go get github.com/gorilla/sessions +``` + +```go +import ( + "github.com/gorilla/sessions" +) +``` + +## Implementation + +A function to create the session middleware, plus a utility function to get a session from +the context: + +```go +func NewSessionMiddleware(store sessions.Store) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("_session_store", store) + return next(c) + } + } +} + +func GetSession(c *echo.Context, name string) (*sessions.Session, error) { + store, err := echo.ContextGet[sessions.Store](c, "_session_store") + if err != nil { + return nil, err + } + return store.Get(c.Request(), name) +} +``` + +The middleware can be initialized like this: + +```go +sessionStore := sessions.NewCookieStore([]byte("secret")) +e.Use(NewSessionMiddleware(sessionStore)) +``` + +## Usage example + +This example exposes two endpoints: `/create-session` creates a new session, and +`/read-session` reads a value from the session if the request contains a session ID. + +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/gorilla/sessions" + "github.com/labstack/echo/v5" +) + +func NewSessionMiddleware(store sessions.Store) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("_session_store", store) + return next(c) + } + } +} + +func GetSession(c *echo.Context, name string) (*sessions.Session, error) { + store, err := echo.ContextGet[sessions.Store](c, "_session_store") + if err != nil { + return nil, fmt.Errorf("failed to get session store: %w", err) + } + return store.Get(c.Request(), name) +} + +func main() { + e := echo.New() + + sessionStore := sessions.NewCookieStore([]byte("secret")) + e.Use(NewSessionMiddleware(sessionStore)) + + e.GET("/create-session", func(c *echo.Context) error { + sess, err := GetSession(c, "session") + if err != nil { + return err + } + sess.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400 * 7, + HttpOnly: true, + } + sess.Values["foo"] = "bar" + if err := sess.Save(c.Request(), c.Response()); err != nil { + return err + } + return c.NoContent(http.StatusOK) + }) + + e.GET("/read-session", func(c *echo.Context) error { + sess, err := GetSession(c, "session") + if err != nil { + return err + } + return c.String(http.StatusOK, fmt.Sprintf("foo=%v\n", sess.Values["foo"])) + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Requesting `/read-session` without providing a session outputs nil as the `foo` value: + +```bash +$ curl -v http://localhost:8080/read-session +* processing: http://localhost:8080/read-session +* Trying [::1]:8080... +* Connected to localhost (::1) port 8080 +> GET /read-session HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/8.2.1 +> Accept: */* +> +< HTTP/1.1 200 OK +< Content-Type: text/plain; charset=UTF-8 +< Date: Thu, 25 Apr 2024 09:15:14 GMT +< Content-Length: 10 +< +foo=<nil> +``` + +Requesting `/create-session` creates a new session: + +```bash +$ curl -v -c cookies.txt http://localhost:8080/create-session +* processing: http://localhost:8080/create-session +* Trying [::1]:8080... +* Connected to localhost (::1) port 8080 +> GET /create-session HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/8.2.1 +> Accept: */* +> +< HTTP/1.1 200 OK +* Added cookie session="..." for domain localhost, path /, expire 1714641420 +< Set-Cookie: session=...; Path=/; Expires=Thu, 02 May 2024 09:17:00 GMT; Max-Age=604800; HttpOnly +< Date: Thu, 25 Apr 2024 09:17:00 GMT +< Content-Length: 0 +< +* Connection #0 to host localhost left intact +``` + +Using the session cookie from the previous response, requesting `/read-session` outputs the +`foo` value from the session: + +```bash +$ curl -v -b cookies.txt http://localhost:8080/read-session +* processing: http://localhost:8080/read-session +* Trying [::1]:8080... +* Connected to localhost (::1) port 8080 +> GET /read-session HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/8.2.1 +> Accept: */* +> Cookie: session=... +> +< HTTP/1.1 200 OK +< Content-Type: text/plain; charset=UTF-8 +< Date: Thu, 25 Apr 2024 09:18:56 GMT +< Content-Length: 8 +< +foo=bar +* Connection #0 to host localhost left intact +``` diff --git a/site/src/content/docs/middleware/static.md b/site/src/content/docs/middleware/static.md new file mode 100644 index 00000000..6194ffba --- /dev/null +++ b/site/src/content/docs/middleware/static.md @@ -0,0 +1,142 @@ +--- +title: Static +description: Serve static files from a root directory. +sidebar: + order: 24 +--- + +Static middleware serves static files from the provided root directory. + +## Usage + +```go +e := echo.New() +e.Use(middleware.Static("/static")) +``` + +This serves static files from the `static` directory. For example, a request to `/js/main.js` +fetches and serves the `static/js/main.js` file. + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Root: "static", + Browse: true, +})) +``` + +This serves static files from the `static` directory and enables directory browsing. + +The default behavior when used with non-root URL paths is to append the URL path to the +filesystem path. + +#### Example 1 + +```go +group := root.Group("somepath") +group.Use(middleware.Static(filepath.Join("filesystempath"))) +// When an incoming request comes for `/somepath`, the actual filesystem request goes to +// `filesystempath/somepath` instead of only `filesystempath`. +group.GET("/*", func(c *echo.Context) error { return echo.ErrNotFound }) +``` + +:::note +Group-level middleware is tied to the route and works only if the group has at least one route. +::: + +:::tip +To turn off this behavior, set the `IgnoreBase` config parameter to `true`. +::: + +#### Example 2 + +Serve SPA assets from an embedded filesystem: + +```go +package main + +import ( + "embed" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +//go:embed assets +var webAssets embed.FS + +func main() { + e := echo.New() + + e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + HTML5: true, + Root: "assets", // files are located in the `assets` directory of the webAssets fs + Filesystem: webAssets, + })) + api := e.Group("/api") + api.GET("/users", func(c *echo.Context) error { + return c.String(http.StatusOK, "users") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Configuration + +```go +type StaticConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Root directory from where the static content is served (relative to given Filesystem). + // `Root: "."` means root folder from Filesystem. + // Required. + Root string + + // Filesystem provides access to the static content. + // Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started). + Filesystem fs.FS + + // Index file for serving a directory. + // Optional. Default value "index.html". + Index string + + // Enable HTML5 mode by forwarding all not-found requests to root so that + // a SPA (single-page application) can handle the routing. + // Optional. Default value false. + HTML5 bool + + // Enable directory browsing. + // Optional. Default value false. + Browse bool + + // Enable ignoring of the base of the URL path. + // Example: when assigning a static middleware to a non-root path group, + // the filesystem path is not doubled. + // Optional. Default value false. + IgnoreBase bool + + // DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when the router is set to + // unescape all parameters and doing it again in this middleware would corrupt the filename that is requested. + DisablePathUnescaping bool + + // DirectoryListTemplate is the template used to list directory contents. + // Optional. Defaults to the `directoryListHTMLTemplate` constant. + DirectoryListTemplate string +} +``` + +### Default configuration + +```go +DefaultStaticConfig = StaticConfig{ + Skipper: DefaultSkipper, + Index: "index.html", +} +``` diff --git a/site/src/content/docs/middleware/trailing-slash.md b/site/src/content/docs/middleware/trailing-slash.md new file mode 100644 index 00000000..2f3f7cfe --- /dev/null +++ b/site/src/content/docs/middleware/trailing-slash.md @@ -0,0 +1,65 @@ +--- +title: Trailing Slash +description: Add or remove a trailing slash from the request URI. +sidebar: + order: 25 +--- + +## Add trailing slash + +Add trailing slash middleware adds a trailing slash to the request URI. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.AddTrailingSlash()) +``` + +## Remove trailing slash + +Remove trailing slash middleware removes a trailing slash from the request URI. + +### Usage + +```go +e := echo.New() +e.Pre(middleware.RemoveTrailingSlash()) +``` + +## Custom configuration + +```go +e := echo.New() +e.Use(middleware.AddTrailingSlashWithConfig(middleware.AddTrailingSlashConfig{ + RedirectCode: http.StatusMovedPermanently, +})) +``` + +The example above adds a trailing slash to the request URI and redirects with +`301 - StatusMovedPermanently`. + +## Configuration + +```go +type AddTrailingSlashConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional, but when provided the request is redirected using this code. + // Valid status codes: [300...308] + RedirectCode int +} +``` + +```go +type RemoveTrailingSlashConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional, but when provided the request is redirected using this code. + RedirectCode int +} +``` diff --git a/site/src/data/github.ts b/site/src/data/github.ts new file mode 100644 index 00000000..e052c8d4 --- /dev/null +++ b/site/src/data/github.ts @@ -0,0 +1,31 @@ +// Build-time GitHub stats. Fetched once during `astro build` and inlined into the +// static HTML — no client JS, no runtime rate limits. The daily deploy cron +// (.github/workflows/deploy.yaml) re-runs the build so the number stays fresh. +// Falls back gracefully so an offline / rate-limited build never fails. +const REPO = 'labstack/echo'; +const FALLBACK_STARS = 32400; + +async function fetchStars(): Promise<number> { + try { + const headers: Record<string, string> = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'echo-docs-build', + }; + // Optional: set GITHUB_TOKEN in CI to lift the 60 req/hr unauthenticated limit. + const token = process.env.GITHUB_TOKEN; + if (token) headers.Authorization = `Bearer ${token}`; + + const res = await fetch(`https://api.github.com/repos/${REPO}`, { headers }); + if (!res.ok) throw new Error(`GitHub API responded ${res.status}`); + const data = await res.json(); + return typeof data.stargazers_count === 'number' ? data.stargazers_count : FALLBACK_STARS; + } catch (e) { + console.warn(`[github] star fetch failed; using fallback ${FALLBACK_STARS}:`, e); + return FALLBACK_STARS; + } +} + +export const stars = await fetchStars(); + +/** e.g. 32412 -> "32.4k" */ +export const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars); diff --git a/site/src/redirects.mjs b/site/src/redirects.mjs new file mode 100644 index 00000000..d753e8d0 --- /dev/null +++ b/site/src/redirects.mjs @@ -0,0 +1,37 @@ +import { readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +// Old Docusaurus URLs -> new locations, generated so astro.config stays small. +// The old site served docs under /docs/ with guide pages flattened (slug +// overrides). On GitHub Pages each entry still emits its own static +// meta-refresh page (no server-side wildcards) — that's why there are ~60. +// On a wildcard-capable host (Cloudflare/Netlify) these collapse to a handful +// of `_redirects` lines. + +const docsDir = fileURLToPath(new URL('./content/docs', import.meta.url)); +const pages = (sub) => + readdirSync(`${docsDir}/${sub}`) + .filter((f) => /\.mdx?$/.test(f)) + .map((f) => f.replace(/\.mdx?$/, '')); + +// Guide pages were flattened to /docs/<name>. Exclude pages that have no old +// URL (new in v5) or are handled explicitly below. +const GUIDE_SKIP = new Set(['context', 'installation', 'quickstart']); + +const fromList = (subPrefix, toPrefix, names) => + Object.fromEntries(names.map((n) => [`/docs/${subPrefix}${n}`, `/${toPrefix}${n}/`])); + +export const redirects = { + // Renamed, removed, index, Docusaurus category pages, and the old search page. + '/docs': '/guide/quickstart/', + '/docs/quick-start': '/guide/quickstart/', + '/docs/start-server': '/guide/customization/', + '/docs/category/guide': '/guide/quickstart/', + '/docs/category/middleware': '/middleware/basic-auth/', + '/docs/category/cookbook': '/cookbook/hello-world/', + '/search': '/', + // Bulk, generated from the current content tree. + ...fromList('', 'guide/', pages('guide').filter((n) => !GUIDE_SKIP.has(n))), + ...fromList('middleware/', 'middleware/', pages('middleware')), + ...fromList('cookbook/', 'cookbook/', pages('cookbook')), +}; diff --git a/site/src/styles/terminal.css b/site/src/styles/terminal.css new file mode 100644 index 00000000..55c8d3dd --- /dev/null +++ b/site/src/styles/terminal.css @@ -0,0 +1,387 @@ +/* Echo Docs — "Terminal" theme over Astro Starlight. + DM Sans prose/headings · Fragment Mono code + UI chrome · warm near-black · Echo cyan. */ + +:root { + --sl-font: 'DM Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; + --sl-font-mono: 'Fragment Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; + --echo-cyan: #00afd1; + --echo-cyan-2: #4ae1ff; + --echo-cyan-text: #33c9e6; /* contrast-tuned for small text/links on dark */ + --echo-soft: rgba(0, 175, 209, 0.12); +} + +/* ---------- DARK (default) ---------- */ +:root[data-theme='dark'] { + --sl-color-accent-low: #052b33; + --sl-color-accent: #00afd1; + --sl-color-accent-high: #8be4f4; + --sl-color-white: #f4f1ef; + --sl-color-gray-1: #ece9e7; + --sl-color-gray-2: #c7c0bb; + --sl-color-gray-3: #aaa19d; + --sl-color-gray-4: #817a76; + --sl-color-gray-5: #2d2724; + --sl-color-gray-6: #1a1614; + --sl-color-black: #0d0b0b; + + --sl-color-bg: #0d0b0b; + --sl-color-bg-nav: rgba(13, 11, 11, 0.85); + --sl-color-bg-sidebar: #0d0b0b; + --sl-color-bg-inline-code: #1c1715; + --sl-color-hairline: #221e1c; + --sl-color-hairline-light: #2d2724; + --sl-color-hairline-shade: #161210; + --sl-color-text: #aaa19d; + --sl-color-text-accent: var(--echo-cyan-text); +} + +/* ---------- LIGHT ---------- */ +:root[data-theme='light'] { + --sl-color-accent-low: #c7eef5; + --sl-color-accent: #0089a4; + --sl-color-accent-high: #005566; + --sl-color-white: #1a1614; + --sl-color-gray-1: #221e1c; + --sl-color-gray-2: #4a4340; + --sl-color-gray-3: #6b6360; + --sl-color-gray-4: #908884; + --sl-color-gray-5: #e7e1dd; + --sl-color-gray-6: #f1ece9; + --sl-color-bg: #faf8f7; + --sl-color-bg-nav: rgba(250, 248, 247, 0.85); + --sl-color-bg-sidebar: #f6f3f1; + --sl-color-bg-inline-code: #efeae7; + --sl-color-hairline: #e7e1dd; + --sl-color-text: #4a4340; + --sl-color-text-accent: #0089a4; +} + +/* atmosphere: warm cyan glow (dark, respects reduced motion) */ +:root[data-theme='dark'] body::before { + content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none; + background: radial-gradient(840px 460px at 84% -12%, rgba(0, 175, 209, 0.10), transparent 62%); +} +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } } + +/* ---------- Type ---------- */ +.sl-markdown-content :is(h1, h2, h3, h4) { font-family: var(--sl-font); font-weight: 700; letter-spacing: -0.018em; } +.sl-markdown-content h1, .content-panel h1 { letter-spacing: -0.03em; } +.sl-markdown-content { font-size: 0.96rem; } + +/* UI chrome in Fragment Mono (terminal feel) */ +.sidebar-content, .sidebar a, nav.sidebar, .right-sidebar, starlight-toc, mobile-starlight-toc, +.pagination-links, .breadcrumbs, site-search button, .sl-badge, .sl-link-button { + font-family: var(--sl-font-mono); +} +.sidebar-content a { font-size: 0.82rem; } +/* sidebar group captions */ +.sidebar-content summary, .top-level > li > a, .sidebar-content > ul > li > details > summary { + text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.68rem; +} +/* active sidebar item: cyan pill + left bar */ +.sidebar a[aria-current='page'] { + color: var(--echo-cyan-text); background: var(--echo-soft); + box-shadow: inset 3px 0 0 var(--echo-cyan); border-radius: 0 7px 7px 0; +} + +/* search box (chrome) */ +site-search button { border-radius: 8px !important; } + +/* code blocks — let Expressive Code round the whole frame as one unit + (title bar: rounded top, code body: rounded bottom). Don't force the inner + <pre> corners or it detaches from the title bar into a second box. */ +.expressive-code { --ec-brdRad: 11px; --ec-brdCol: var(--sl-color-hairline); } +.sl-markdown-content code { font-size: 0.86em; } + +/* Retheme Expressive Code to our warm palette (dark) so code blocks match the + site instead of github-dark's cool gray, and remove the title-bar seam. */ +[data-theme='dark'] .expressive-code { + --ec-codeBg: #0a0908; + --ec-frm-edBg: #0a0908; + --ec-frm-trmBg: #0a0908; + --ec-frm-edTabBarBg: #100e0d; + --ec-frm-edTabBarBrdBtmCol: var(--sl-color-hairline); + --ec-frm-edActTabBg: #0a0908; + --ec-frm-edActTabIndTopCol: var(--echo-cyan); + --ec-frm-trmTtbBg: #100e0d; + --ec-frm-trmTtbBrdBtmCol: var(--sl-color-hairline); + --ec-frm-trmTtbDotsOpa: 0.55; + --ec-frm-trmTtbFg: var(--sl-color-gray-4); + --ec-brdCol: var(--sl-color-hairline); + --ec-frm-frameBoxShdCssVal: none; + --ec-frm-inlineBtnBrd: transparent; + --ec-frm-inlineBtnBrdHov: transparent; + /* "Copied!" success tooltip — on-brand cyan bubble with dark text */ + --ec-frm-tooltipSuccessBg: var(--echo-cyan); + --ec-frm-tooltipSuccessFg: #0d0b0b; +} +/* Copy button: EC draws its boxed border on the button's ::before pseudo — + kill that, keep a subtle fill that warms to cyan, and tint the icon on hover. */ +.expressive-code .copy button { + border: none !important; + background: color-mix(in srgb, #fff 4%, transparent); + border-radius: 7px; +} +.expressive-code .copy button::before { border: none !important; background: transparent !important; } +.expressive-code .copy button:hover { background: color-mix(in srgb, var(--echo-cyan) 16%, transparent); } +.expressive-code .copy button:hover::after { background-color: var(--echo-cyan-2) !important; } /* icon glyph */ + +/* ---------- Doc action toolbar ---------- */ +.doc-actions { display: flex; flex-wrap: wrap; gap: 7px; margin: 4px 0 4px; } +.doc-act { + display: inline-flex; align-items: center; gap: 6px; font-family: var(--sl-font-mono); + font-size: 0.72rem; color: var(--sl-color-gray-3); border: 1px solid var(--sl-color-hairline-light); + background: var(--sl-color-bg-sidebar); border-radius: 7px; padding: 5px 11px; cursor: pointer; + line-height: 1; text-decoration: none; transition: all .15s; +} +.doc-act:hover { color: var(--sl-color-white); border-color: var(--echo-cyan); text-decoration: none; } +.doc-act .ph { font-size: 14px; } +.doc-act--primary { background: var(--echo-soft); border-color: transparent; color: var(--echo-cyan-text); } + +/* ---------- Ask Echo (island) ---------- */ +.ask-fab { + position: fixed; right: 22px; bottom: 22px; z-index: 999; display: inline-flex; align-items: center; gap: 8px; + background: var(--sl-color-bg-sidebar); border: 1px solid var(--sl-color-hairline-light); color: var(--sl-color-white); + border-radius: 30px; padding: 9px 16px; font-family: var(--sl-font-mono); font-size: 0.78rem; cursor: pointer; + box-shadow: 0 14px 34px -12px rgba(0,0,0,.7); +} +.ask-fab:hover { border-color: var(--echo-cyan); } +.ask-fab .ph { color: var(--echo-cyan-text); font-size: 16px; } +.ask-fab kbd { background: var(--sl-color-hairline); border: 1px solid var(--sl-color-hairline-light); border-radius: 5px; padding: 1px 6px; font-size: 0.7rem; color: var(--sl-color-gray-4); } +.ask-overlay { position: fixed; inset: 0; z-index: 1000; background: rgba(6,5,5,.66); backdrop-filter: blur(6px); display: flex; align-items: flex-start; justify-content: center; padding-top: 11vh; } +.ask-overlay[hidden] { display: none; } +.ask-fab[hidden] { display: none; } +.ask-palette { width: min(660px, 92vw); background: var(--sl-color-bg-sidebar); border: 1px solid var(--sl-color-hairline-light); border-radius: 16px; overflow: hidden; box-shadow: 0 40px 120px -30px rgba(0,0,0,.9); font-family: var(--sl-font-mono); } +.ask-top { display: flex; align-items: center; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--sl-color-hairline); } +.ask-top .ph { color: var(--echo-cyan-text); font-size: 19px; } +.ask-input { flex: 1; background: none; border: 0; outline: none; color: var(--sl-color-white); font-size: 15px; font-family: var(--sl-font-mono); } +.ask-input::placeholder { color: var(--sl-color-gray-4); } +.ask-esc { font-size: 0.7rem; color: var(--sl-color-gray-4); border: 1px solid var(--sl-color-hairline-light); border-radius: 6px; padding: 3px 7px; } +.ask-body { max-height: 54vh; overflow-y: auto; padding: 12px 16px 16px; } +.ask-s { display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; padding: 10px 11px; border: 0; background: none; border-radius: 8px; color: var(--sl-color-gray-3); font-size: 13px; cursor: pointer; font-family: var(--sl-font-mono); } +.ask-s:hover { background: var(--sl-color-hairline); color: var(--sl-color-white); } +.ask-s .ph { font-size: 17px; color: var(--echo-cyan-text); } +.ask-badge { display: inline-flex; align-items: center; gap: 8px; font-size: 0.7rem; color: var(--echo-cyan-text); background: var(--echo-soft); border-radius: 20px; padding: 4px 11px; margin-bottom: 13px; } +.ask-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--echo-cyan); animation: askb 1.2s infinite; } +@keyframes askb { 50% { opacity: .3; } } +.ask-answer { font-size: 13.5px; line-height: 1.7; color: var(--sl-color-text); } +.ask-answer strong { color: var(--sl-color-white); } +.ask-answer pre { margin: 12px 0; background: #0a0908; border: 1px solid var(--sl-color-hairline); border-radius: 9px; padding: 14px 16px; overflow-x: auto; font-size: 12.5px; } +.ask-cursor { display: inline-block; width: 7px; height: 15px; background: var(--echo-cyan); vertical-align: -2px; animation: askb .9s infinite; } +.ask-sources { margin-top: 16px; border-top: 1px solid var(--sl-color-hairline); padding-top: 12px; } +.ask-sources h5 { font-size: 0.62rem; text-transform: uppercase; letter-spacing: .1em; color: var(--sl-color-gray-4); margin-bottom: 8px; } +.ask-src { display: flex; gap: 9px; padding: 6px 8px; border-radius: 7px; color: var(--sl-color-gray-3); font-size: 12.5px; } +.ask-n { font-size: 0.7rem; color: var(--echo-cyan-text); border: 1px solid var(--sl-color-hairline-light); border-radius: 5px; padding: 1px 6px; } + +/* ---------- Homepage ---------- */ +.hero { padding-block: 3rem 1rem; } +.hero h1 { font-weight: 800; letter-spacing: -0.035em; } +.hero .tagline { font-family: var(--sl-font-mono); font-size: 0.95rem; } +.echo-cw { border: 1px solid var(--sl-color-hairline-light); border-radius: 13px; overflow: hidden; background: #0a0908; box-shadow: 0 30px 80px -40px rgba(0,0,0,.8); text-align: left; } +.echo-cw .bar { display: flex; align-items: center; gap: 7px; padding: 11px 14px; border-bottom: 1px solid var(--sl-color-hairline); } +.echo-cw .bar i { width: 11px; height: 11px; border-radius: 50%; display: inline-block; } +.echo-cw .bar .fn { margin-left: 8px; font-family: var(--sl-font-mono); font-size: 11.5px; color: var(--sl-color-gray-4); } +.echo-cw pre { margin: 0; padding: 16px 18px; font-family: var(--sl-font-mono); font-size: 12.5px; line-height: 1.8; overflow: auto; color: var(--sl-color-text); } +.echo-cw .kw { color: var(--echo-cyan-2); } .echo-cw .str { color: #c79a6b; } .echo-cw .cm { color: #5f5853; } +.echo-cw .run { border-top: 1px solid var(--sl-color-hairline); padding: 11px 18px; font-family: var(--sl-font-mono); font-size: 12px; color: var(--sl-color-gray-4); } +.echo-cw .run .ok { color: #28c840; } + +.echo-stats { display: flex; gap: 38px; flex-wrap: wrap; justify-content: center; padding: 26px 0; margin: 8px 0; border-block: 1px solid var(--sl-color-hairline); font-family: var(--sl-font-mono); } +.echo-stats b { display: block; font-family: var(--sl-font); font-weight: 700; font-size: 1.6rem; color: var(--sl-color-white); letter-spacing: -0.02em; } +.echo-stats span { font-size: 0.8rem; color: var(--sl-color-gray-4); } +.echo-features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 28px; } +@media (max-width: 800px) { .echo-features { grid-template-columns: 1fr; } } +.echo-card { border: 1px solid var(--sl-color-hairline); background: var(--sl-color-bg-sidebar); border-radius: 13px; padding: 20px; } +.echo-card .ph { font-size: 22px; color: var(--echo-cyan-text); } +.echo-card h3 { font-family: var(--sl-font); font-weight: 600; font-size: 1.05rem; margin: 12px 0 6px; color: var(--sl-color-white); } +.echo-card p { font-size: 0.9rem; color: var(--sl-color-gray-3); margin: 0; line-height: 1.62; } + +/* install command pill */ +.echo-install { display: flex; justify-content: center; margin: -4px 0 4px; } +.echo-install code { + font-family: var(--sl-font-mono); font-size: 13.5px; color: var(--sl-color-white); + background: #0a0908; border: 1px solid var(--sl-color-hairline-light); border-radius: 10px; padding: 11px 18px; +} +.echo-install .p { color: var(--echo-cyan-text); margin-right: 8px; } +.echo-install .c { color: var(--sl-color-gray-4); margin-left: 10px; border-left: 1px solid var(--sl-color-hairline-light); padding-left: 12px; } + +/* section heading */ +.echo-h { text-align: center; margin: 56px 0 26px; } +.echo-h .ey { font-family: var(--sl-font); font-weight: 600; font-size: 0.72rem; letter-spacing: .15em; text-transform: uppercase; color: var(--echo-cyan-text); } +.echo-h h2 { font-family: var(--sl-font); font-weight: 700; font-size: 1.7rem; letter-spacing: -0.02em; color: var(--sl-color-white); margin: 8px 0 0; border: 0; padding: 0; } + +/* get-started steps */ +.echo-steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } +@media (max-width: 800px) { .echo-steps { grid-template-columns: 1fr; } } +.echo-step { position: relative; border: 1px solid var(--sl-color-hairline); border-radius: 12px; padding: 22px 22px 20px; background: var(--sl-color-bg-sidebar); transition: border-color .15s; } +.echo-step:hover { border-color: rgba(0,175,209,.4); } +.echo-step .n { display: block; font-family: var(--sl-font-mono); font-size: 1.9rem; font-weight: 700; line-height: 1; letter-spacing: -0.02em; background: linear-gradient(180deg, var(--echo-cyan-2), var(--echo-cyan)); -webkit-background-clip: text; background-clip: text; color: transparent; margin: 0 0 14px; } +.echo-step h4 { font-family: var(--sl-font); font-weight: 600; font-size: 0.95rem; color: var(--sl-color-white); margin: 0 0 6px; } +.echo-step p { font-size: 0.9rem; color: var(--sl-color-gray-3); margin: 0 0 12px; } +.echo-step pre { margin: 0; background: #0a0908; border: 1px solid var(--sl-color-hairline); border-radius: 8px; padding: 10px 12px; font-family: var(--sl-font-mono); font-size: 11.5px; color: var(--sl-color-text); overflow-x: auto; } + +/* footer */ +.echo-foot { margin-top: 64px; border-top: 1px solid var(--sl-color-hairline); padding-top: 34px; display: flex; flex-wrap: wrap; gap: 30px 56px; } +.echo-foot .brand { flex: 1 1 200px; } +.echo-foot .brand .n { font-family: var(--sl-font); font-weight: 700; font-size: 1.05rem; color: var(--sl-color-white); } +.echo-foot .brand p { font-family: var(--sl-font-mono); font-size: 0.74rem; color: var(--sl-color-gray-4); margin: 8px 0 0; max-width: 30ch; } +.echo-foot .col h4 { font-family: var(--sl-font); font-size: 0.66rem; text-transform: uppercase; letter-spacing: .12em; color: var(--sl-color-gray-4); margin: 0 0 12px; } +.echo-foot .col a { display: block; font-family: var(--sl-font-mono); font-size: 0.875rem; color: var(--sl-color-gray-3); padding: 4px 0; text-decoration: none; } +.echo-foot .col a:hover { color: var(--echo-cyan-text); } +.echo-copy { width: 100%; margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--sl-color-hairline); font-family: var(--sl-font-mono); font-size: 0.72rem; color: var(--sl-color-gray-4); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } + +/* social icon row — used in both homepage footer and the site-wide docs footer */ +.echo-social { display: inline-flex; align-items: center; gap: 16px; } +.echo-social a { display: inline-flex; color: var(--sl-color-gray-3); font-size: 1.2rem; line-height: 1; text-decoration: none; transition: color .15s; } +.echo-social a:hover { color: var(--echo-cyan-text); } + +/* site-wide footer on docs pages (homepage uses its own .echo-foot) */ +.echo-docfoot { margin-top: 48px; padding-top: 20px; border-top: 1px solid var(--sl-color-hairline); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; font-family: var(--sl-font-mono); font-size: 0.72rem; color: var(--sl-color-gray-4); } + +/* ===================== Asides / callouts ===================== */ +/* Starlight defaults (purple note, muddy yellow caution) clash with the warm + terminal palette. Re-theme: faint tinted bg, solid accent left-bar, mono + accent title. Each variant just sets --aside-accent / --aside-text. */ +.starlight-aside { + --aside-accent: var(--echo-cyan); + --aside-text: var(--echo-cyan-text); + border: 1px solid color-mix(in srgb, var(--aside-accent) 25%, transparent); + border-inline-start: 3px solid var(--aside-accent); + border-radius: 9px; + background: color-mix(in srgb, var(--aside-accent) 8%, var(--sl-color-bg)); + padding: 14px 18px; +} +.starlight-aside__title { + color: var(--aside-text); + font-family: var(--sl-font-mono); + font-weight: 600; + font-size: 0.82rem; + letter-spacing: 0.01em; + display: flex; + align-items: center; + gap: 9px; +} +.starlight-aside__icon { color: var(--aside-accent); } +.starlight-aside__content { color: var(--sl-color-gray-2); font-size: 0.92rem; } +.starlight-aside__content a { color: var(--aside-text); text-underline-offset: 2px; } + +.starlight-aside--note { --aside-accent: #00afd1; --aside-text: #5fd6ec; } +.starlight-aside--tip { --aside-accent: #3fb950; --aside-text: #6fdc82; } +.starlight-aside--caution { --aside-accent: #d9a23b; --aside-text: #f0c674; } +.starlight-aside--danger { --aside-accent: #f06f6f; --aside-text: #ff9b9b; } + +/* ===================== Prev / next pager ===================== */ +/* Starlight's default title is ~h4; in mono it reads oversized. Tone down, + tighten padding, add a cyan hover border to match other interactive cards. */ +.pagination-links a { padding: 14px 18px; border-radius: 10px; transition: border-color .15s, background .15s; } +.pagination-links a:hover { border-color: var(--echo-cyan); } +.pagination-links a > span { font-size: 0.68rem; color: var(--sl-color-gray-4); } +.pagination-links a .link-title { font-size: 1.02rem; font-weight: 600; margin-top: 2px; } + +/* ===================== Theme polish ===================== */ + +/* Tables — mono accent headers, hairline rules, subtle row hover */ +.sl-markdown-content table { border-collapse: collapse; width: 100%; font-size: 0.9rem; } +.sl-markdown-content th, .sl-markdown-content td { border: 1px solid var(--sl-color-hairline); padding: 8px 13px; } +.sl-markdown-content th { + font-family: var(--sl-font-mono); font-weight: 600; font-size: 0.74rem; + text-transform: uppercase; letter-spacing: 0.05em; text-align: start; + color: var(--echo-cyan-text); background: var(--sl-color-bg-sidebar); +} +.sl-markdown-content tbody tr:hover td { background: color-mix(in srgb, var(--echo-cyan) 5%, transparent); } + +/* Blockquotes — cyan accent + faint tint instead of the plain gray bar */ +.sl-markdown-content blockquote { + border-inline-start: 3px solid var(--echo-cyan); + background: color-mix(in srgb, var(--echo-cyan) 5%, transparent); + border-radius: 0 8px 8px 0; padding: 2px 18px; color: var(--sl-color-gray-2); +} + +/* Sidebar + TOC current item in Echo cyan */ +.sidebar-content a[aria-current='page'] { color: var(--echo-cyan-text); font-weight: 600; } +starlight-toc a[aria-current='true'] { color: var(--echo-cyan-text); border-inline-start-color: var(--echo-cyan); } + +/* Prose links — cyan with a softer underline that firms up on hover. + Scoped to text containers only so it never overrides component buttons/cards + (e.g. the homepage .hh-btn CTAs, footer links) that live in .sl-markdown-content. */ +.sl-markdown-content :is(p, li, td, dd) a { + color: var(--echo-cyan-text); text-underline-offset: 3px; text-decoration-thickness: 1px; + text-decoration-color: color-mix(in srgb, var(--echo-cyan) 45%, transparent); +} +.sl-markdown-content :is(p, li, td, dd) a:hover { text-decoration-color: var(--echo-cyan); } + +/* Thin, terminal-flavored scrollbar */ +* { scrollbar-width: thin; scrollbar-color: #3a3433 transparent; } +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: #3a3433; border-radius: 6px; border: 2px solid var(--sl-color-bg); } +::-webkit-scrollbar-thumb:hover { background: var(--echo-cyan); } + +/* ===================== Living Terminal hero ===================== */ +/* Home uses a custom <HomeHero/>; hide Starlight's own splash hero/title region. */ +.sl-markdown-content + .hero, .content-panel .hero, main > .hero { display: none; } +:where(.hero) { display: none; } + +/* Starlight adds margin-top to every adjacent sibling inside .sl-markdown-content, + which knocks the 2nd+ item of each flex/grid row out of alignment. Our home rows + manage their own spacing via gap/explicit margins — neutralise the injected margin. */ +.hh > *, .hh-left > *, .hh-win .bar > *, +.echo-stats > *, .echo-features > *, .echo-steps > *, .echo-eco > *, +.sp-featured > *, .sp-card > *, .sp-grid > *, +.echo-foot > .brand, .echo-foot > .col { margin-top: 0; } +.hh-win .bar i { align-self: center; } + +.hh { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: start; padding: 28px 0 8px; } +@media (max-width: 900px) { .hh { grid-template-columns: 1fr; gap: 28px; } } +.hh-eyebrow { display: inline-flex; align-items: center; gap: 7px; font-family: var(--sl-font-mono); font-size: 0.72rem; color: var(--echo-cyan-text); border: 1px solid rgba(0,175,209,.3); background: var(--echo-soft); border-radius: 30px; padding: 4px 12px; margin-bottom: 22px; } +.hh h1 { font-family: var(--sl-font); font-weight: 800; font-size: clamp(2.2rem, 4.4vw, 3.2rem); line-height: 1.05; letter-spacing: -0.035em; color: var(--sl-color-white); margin: 0 0 18px; } +.hh h1 .g { color: var(--echo-cyan-text); } +.hh-sub { font-family: var(--sl-font-mono); font-size: 1rem; line-height: 1.72; color: var(--sl-color-text); max-width: 48ch; margin-bottom: 26px; } +.hh-cta { display: flex; gap: 11px; flex-wrap: wrap; align-items: center; margin-bottom: 20px; } +.hh-btn { display: inline-flex; align-items: center; gap: 8px; font-family: var(--sl-font); font-weight: 600; font-size: 0.92rem; border-radius: 10px; padding: 11px 20px; cursor: pointer; text-decoration: none; transition: all .16s; } +.hh-btn.pri { background: linear-gradient(145deg, #00afd1, #4ae1ff); color: #04110c; } +.hh-btn.pri:hover { box-shadow: 0 10px 26px -8px var(--echo-cyan); } +.hh-btn.sec { border: 1px solid var(--sl-color-hairline-light); color: var(--sl-color-white); background: var(--sl-color-bg-sidebar); } +.hh-btn.sec:hover { border-color: var(--echo-cyan); } +.hh-install { display: inline-flex; align-items: center; gap: 10px; font-family: var(--sl-font-mono); font-size: 0.9rem; color: var(--sl-color-text); border: 1px solid var(--sl-color-hairline-light); background: #0a0908; border-radius: 9px; padding: 10px 15px; cursor: pointer; } +.hh-install .p { color: var(--echo-cyan-text); } +.hh-install .cp { margin-left: 4px; color: var(--sl-color-gray-4); } +.hh-install:hover .cp { color: var(--echo-cyan-text); } + +/* terminal window */ +.hh-win { border: 1px solid var(--sl-color-hairline-light); border-radius: 13px; overflow: hidden; background: #0a0908; box-shadow: 0 30px 80px -40px rgba(0,0,0,.85); font-family: var(--sl-font-mono); } +.hh-win .bar { display: flex; align-items: center; gap: 7px; padding: 11px 14px; border-bottom: 1px solid var(--sl-color-hairline); } +.hh-win .bar i { width: 11px; height: 11px; border-radius: 50%; display: inline-block; } +.hh-win .bar .fn { margin-left: 8px; font-size: 11.5px; color: var(--sl-color-gray-4); } +.hh-win pre { margin: 0; padding: 15px 18px; font-size: 12.3px; line-height: 1.75; color: var(--sl-color-text); overflow-x: auto; } +.hh-win .kw { color: var(--echo-cyan-2); } .hh-win .str { color: #c79a6b; } .hh-win .cm { color: #5f5853; } .hh-win .fnn { color: #d6cfc9; } +.hh-term { border-top: 1px solid var(--sl-color-hairline); background: #08100f; padding: 14px 18px; font-size: 12.3px; line-height: 1.85; min-height: 118px; } +.hh-term .pr { color: var(--echo-cyan-text); } +.hh-term .dim { color: var(--sl-color-gray-4); } +.hh-term .ok { color: #28c840; } +.hh-term .json { color: #9fd6cf; } +.hh-cur { display: inline-block; width: 7px; height: 14px; background: var(--echo-cyan); vertical-align: -2px; animation: hhblink 1s steps(2,start) infinite; } +@keyframes hhblink { 50% { opacity: 0; } } + +/* ===================== Sponsors ===================== */ +.sp-featured { display: flex; align-items: center; gap: 16px; justify-content: center; border: 1px solid var(--sl-color-hairline-light); background: var(--sl-color-bg-sidebar); border-radius: 14px; padding: 20px 26px; max-width: 560px; margin: 0 auto 18px; text-decoration: none; transition: border-color .15s; } +.sp-featured:hover { border-color: var(--echo-cyan); } +.sp-featured .t b, .sp-featured .t span { text-decoration: none; } +.sp-featured img { width: 44px; height: 44px; border-radius: 10px; } +.sp-featured .t b { font-family: var(--sl-font); font-weight: 700; color: var(--sl-color-white); font-size: 1rem; } +.sp-featured .t span { display: block; font-family: var(--sl-font-mono); font-size: 0.86rem; color: var(--sl-color-gray-4); margin-top: 4px; } +.sp-grid { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; margin-bottom: 22px; } +.sp-card { display: flex; align-items: center; gap: 10px; border: 1px solid var(--sl-color-hairline); border-radius: 10px; padding: 10px 14px; text-decoration: none; transition: border-color .15s; } +.sp-card:hover { border-color: var(--echo-cyan); } +.sp-card img { width: 26px; height: 26px; border-radius: 50%; } +.sp-card span { font-family: var(--sl-font-mono); font-size: 0.9rem; color: var(--sl-color-gray-3); } +.sp-cta { text-align: center; } +.sp-cta a { font-family: var(--sl-font-mono); font-size: 0.82rem; color: var(--echo-cyan-text); text-decoration: none; border: 1px solid rgba(0,175,209,.3); border-radius: 9px; padding: 8px 16px; } +.sp-cta a:hover { background: var(--echo-soft); } + +/* ===================== Ecosystem ===================== */ +.echo-eco { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } +@media (max-width: 800px) { .echo-eco { grid-template-columns: 1fr 1fr; } } +.eco-card { border: 1px solid var(--sl-color-hairline); border-radius: 11px; padding: 16px; text-decoration: none; transition: border-color .15s, transform .15s; } +.eco-card:hover { border-color: var(--echo-cyan); transform: translateY(-2px); } +.eco-card h4 { font-family: var(--sl-font-mono); font-weight: 500; font-size: 0.92rem; color: var(--echo-cyan-text); margin: 0 0 6px; } +.eco-card p { font-family: var(--sl-font); font-size: 0.86rem; color: var(--sl-color-gray-3); margin: 0; line-height: 1.55; } diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 00000000..92a18df9 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"], + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + } +} diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 0b70ea66..5fe80262 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -9,7 +9,7 @@ const darkTheme = themes.dracula; const config = { title: 'Echo', tagline: 'High performance, extensible, minimalist Go web framework', - favicon: 'img/favicon.ico', + favicon: 'img/favicon.svg', // Set the production url of your site here url: 'https://echo.labstack.com/', @@ -32,9 +32,16 @@ const config = { // to replace "en" with "zh-Hans". i18n: { defaultLocale: 'en', - locales: ['en'], + locales: ['en', 'zh-Hans', 'ja', 'es', 'fr'], }, + // Phosphor icon set (used by the Ask Echo palette and doc action toolbar). + stylesheets: [ + 'https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css', + 'https://unpkg.com/@phosphor-icons/web@2.1.1/src/fill/style.css', + 'https://unpkg.com/@phosphor-icons/web@2.1.1/src/duotone/style.css', + ], + presets: [ [ 'classic', @@ -103,8 +110,12 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ - // Replace with your project's social card - image: 'img/docusaurus-social-card.jpg', + // Echo social card (1200×630) — the old docusaurus-social-card.jpg never existed. + image: 'img/echo-social-card.png', + colorMode: { + defaultMode: 'dark', + respectPrefersColorScheme: false, + }, navbar: { logo: { alt: 'Echo', @@ -129,6 +140,10 @@ const config = { label: 'GitHub', position: 'right', }, + { + type: 'localeDropdown', + position: 'right', + }, ], }, footer: { diff --git a/website/src/components/AskEcho.js b/website/src/components/AskEcho.js new file mode 100644 index 00000000..a9972cec --- /dev/null +++ b/website/src/components/AskEcho.js @@ -0,0 +1,158 @@ +import React, {useState, useEffect, useRef, useCallback} from 'react'; + +// Quick-start prompts shown before the user types. +const SUGGESTIONS = [ + {icon: 'ph-lock-key', q: 'How do I add JWT authentication?'}, + {icon: 'ph-globe', q: 'How do I enable CORS?'}, + {icon: 'ph-link', q: 'How do I bind a JSON request body?'}, + {icon: 'ph-folder-open', q: 'How do I serve static files?'}, +]; + +// Demo answer used by the prototype. To make this real, replace `streamAnswer` +// with a call to your RAG provider, e.g. kapa.ai: +// const r = await fetch('https://api.kapa.ai/query/v1/projects/<id>/chat/', +// {method:'POST', headers:{'X-API-KEY': KEY,'Content-Type':'application/json'}, +// body: JSON.stringify({query})}); +// then stream r.body. The UI below is provider-agnostic. +const DEMO_ANSWER = +`To add JWT authentication, use Echo's built-in <strong>JWT middleware</strong>. Register it on the routes (or group) you want to protect: + +<pre><code>import echojwt "github.com/labstack/echo-jwt/v4" + +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("your-secret"), +}))</code></pre> + +Requests must then send <code>Authorization: Bearer <token></code>. Inside a handler, read claims via <code>c.Get("user")</code>. For login, issue a token with <strong>github.com/golang-jwt/jwt/v5</strong>.`; + +const SOURCES = [ + 'Guide › Middleware › JWT', + 'Cookbook › JWT Authentication', + 'API › echo-jwt', +]; + +export default function AskEcho() { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const [answer, setAnswer] = useState(''); + const [thinking, setThinking] = useState(false); + const [done, setDone] = useState(false); + const inputRef = useRef(null); + const timer = useRef(null); + + const reset = () => { + setQuery(''); setAnswer(''); setThinking(false); setDone(false); + if (timer.current) clearInterval(timer.current); + }; + const close = useCallback(() => { setOpen(false); reset(); }, []); + const openPalette = useCallback(() => { reset(); setOpen(true); }, []); + + useEffect(() => { + const onKey = (e) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + setOpen((o) => { if (o) reset(); return !o; }); + } + if (e.key === 'Escape') close(); + }; + const onOpen = () => openPalette(); + window.addEventListener('keydown', onKey); + window.addEventListener('ask-echo-open', onOpen); + return () => { + window.removeEventListener('keydown', onKey); + window.removeEventListener('ask-echo-open', onOpen); + if (timer.current) clearInterval(timer.current); + }; + }, [close, openPalette]); + + useEffect(() => { + if (open && inputRef.current) setTimeout(() => inputRef.current.focus(), 40); + }, [open]); + + // Typewriter stream of the (demo) answer. + const ask = (q) => { + setQuery(q); setThinking(true); setAnswer(''); setDone(false); + if (timer.current) clearInterval(timer.current); + setTimeout(() => { + setThinking(false); + let i = 0; + timer.current = setInterval(() => { + i += 5; + setAnswer(DEMO_ANSWER.slice(0, i)); + if (i >= DEMO_ANSWER.length) { + clearInterval(timer.current); + setAnswer(DEMO_ANSWER); + setDone(true); + } + }, 12); + }, 450); + }; + + if (!open) { + return ( + <button className="ask-fab" onClick={openPalette} aria-label="Ask Echo"> + <i className="ph ph-sparkle" /> Ask Echo <kbd>⌘K</kbd> + </button> + ); + } + + return ( + <div className="ask-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) close(); }}> + <div className="ask-palette" role="dialog" aria-label="Ask Echo"> + <div className="ask-top"> + <i className="ph ph-sparkle ask-spark" /> + <input + ref={inputRef} + className="ask-input" + placeholder="Ask Echo a question…" + value={query} + onChange={(e) => setQuery(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter' && query.trim()) ask(query.trim()); }} + autoComplete="off" + /> + <span className="ask-esc">ESC</span> + </div> + + <div className="ask-body"> + {!answer && !thinking && ( + <div className="ask-suggest"> + {SUGGESTIONS.map((s) => ( + <button key={s.q} className="ask-s" onClick={() => ask(s.q)}> + <i className={`ph ${s.icon}`} /> {s.q} + <span className="ask-k">↵</span> + </button> + ))} + </div> + )} + + {thinking && ( + <div className="ask-badge"><span className="ask-dot" /> Ask Echo is thinking…</div> + )} + + {answer && ( + <> + <div className="ask-badge"><span className="ask-dot" /> Ask Echo</div> + <div + className="ask-answer" + dangerouslySetInnerHTML={{ __html: answer + (done ? '' : '<span class="ask-cursor"></span>') }} + /> + {done && ( + <div className="ask-sources"> + <h5>Sources</h5> + {SOURCES.map((s, i) => ( + <div className="ask-src" key={s}><span className="ask-n">{i + 1}</span> {s}</div> + ))} + </div> + )} + </> + )} + </div> + + <div className="ask-foot"> + <span><b>↵</b> ask</span><span><b>esc</b> close</span> + <span style={{marginLeft: 'auto'}}>Powered by <b>Ask Echo</b> · answers in your language</span> + </div> + </div> + </div> + ); +} diff --git a/website/src/components/DocActions.js b/website/src/components/DocActions.js new file mode 100644 index 00000000..145fa583 --- /dev/null +++ b/website/src/components/DocActions.js @@ -0,0 +1,44 @@ +import React, {useState} from 'react'; +import {useLocation} from '@docusaurus/router'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; + +// Toolbar rendered at the top of every doc page: Ask Echo + "open in AI" deep links. +export default function DocActions() { + const {siteConfig} = useDocusaurusContext(); + const {pathname} = useLocation(); + const [copied, setCopied] = useState(false); + + const url = (siteConfig.url || '') + (pathname || ''); + const prompt = + `I'm reading the Echo (Go web framework) docs page: ${url} — help me understand it and write example code.`; + const gpt = 'https://chatgpt.com/?q=' + encodeURIComponent(prompt); + const claude = 'https://claude.ai/new?q=' + encodeURIComponent(prompt); + + const ask = () => { + if (typeof window !== 'undefined') window.dispatchEvent(new Event('ask-echo-open')); + }; + const copy = () => { + if (typeof navigator !== 'undefined' && navigator.clipboard) { + navigator.clipboard.writeText(url); + setCopied(true); + setTimeout(() => setCopied(false), 1400); + } + }; + + return ( + <div className="doc-actions"> + <button className="doc-act doc-act--primary" onClick={ask}> + <i className="ph ph-sparkle" /> Ask Echo + </button> + <button className="doc-act" onClick={copy}> + <i className={`ph ${copied ? 'ph-check' : 'ph-copy'}`} /> {copied ? 'Copied' : 'Copy'} + </button> + <a className="doc-act" href={gpt} target="_blank" rel="noopener noreferrer"> + <i className="ph ph-chat-circle-dots" /> ChatGPT + </a> + <a className="doc-act" href={claude} target="_blank" rel="noopener noreferrer"> + <i className="ph ph-asterisk" /> Claude + </a> + </div> + ); +} diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 13c3f7a5..59f850db 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -1,31 +1,245 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Fragment+Mono:ital@0;1&display=swap'); + /** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. + * Echo docs theme — terminal-precise, 2026. + * Fragment Mono body + DM Sans headings (the exact fonts docs.openclaw.ai + * declares in its Mintlify config) on a warm near-black. Echo-cyan accent. + * Pure Infima variable + component overrides (no swizzle dependency). */ -/* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #007a92; - --ifm-color-primary-dark: #006e83; - --ifm-color-primary-darker: #00687c; - --ifm-color-primary-darkest: #005566; - --ifm-color-primary-light: #0086a1; - --ifm-color-primary-lighter: #008ca8; - --ifm-color-primary-lightest: #009fbe; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); -} - -/* For readability concerns, you should choose a lighter palette in dark mode. */ + /* TYPE — Fragment Mono body (the defining trait), DM Sans headings */ + --ifm-font-family-base: "Fragment Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + --ifm-font-family-monospace: "Fragment Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + --ifm-heading-font-family: "DM Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --ifm-heading-font-weight: 700; + --ifm-font-size-base: 15px; + --ifm-line-height-base: 1.65; + --ifm-code-font-size: 88%; + --ifm-global-radius: 9px; + --ifm-spacing-horizontal: 1.5rem; + + /* brand cyan (logo) — darker for light-mode contrast */ + --ifm-color-primary: #0089a4; + --ifm-color-primary-dark: #007b93; + --ifm-color-primary-darker: #00748b; + --ifm-color-primary-darkest: #005f72; + --ifm-color-primary-light: #0097b5; + --ifm-color-primary-lighter: #009ebd; + --ifm-color-primary-lightest: #00b3d6; + + /* light surfaces — warm paper */ + --ifm-background-color: #faf8f7; + --ifm-background-surface-color: #ffffff; + --ifm-heading-color: #1a1614; + --ifm-font-color-base: #4a4340; + --ifm-color-emphasis-200: #ece7e4; + --ifm-color-emphasis-300: #ddd6d2; + --docusaurus-highlighted-code-line-bg: rgba(0, 175, 209, 0.1); + --echo-glow: 0 0 0 0 transparent; +} + +/* ====================== DARK (default) ====================== */ [data-theme='dark'] { - --ifm-color-primary: #5db7de; - --ifm-color-primary-dark: #43abd9; - --ifm-color-primary-darker: #36a6d6; - --ifm-color-primary-darkest: #258bb7; - --ifm-color-primary-light: #77c3e3; - --ifm-color-primary-lighter: #84c8e6; - --ifm-color-primary-lightest: #acdaee; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + /* logo cyan, brightened for dark legibility */ + --ifm-color-primary: #2cc6e6; + --ifm-color-primary-dark: #16bce0; + --ifm-color-primary-darker: #00afd1; + --ifm-color-primary-darkest: #008ba6; + --ifm-color-primary-light: #46cfeb; + --ifm-color-primary-lighter: #61d8ef; + --ifm-color-primary-lightest: #8be4f4; + + --echo-accent: #00afd1; + --echo-accent-2: #4ae1ff; + --echo-accent-soft: rgba(0, 175, 209, 0.13); + + /* warm near-black surfaces (matches docs.openclaw.ai) */ + --ifm-background-color: #0d0b0b; + --ifm-background-surface-color: #151210; + --ifm-navbar-background-color: rgba(13, 11, 11, 0.72); + --ifm-footer-background-color: #0a0908; + --ifm-card-background-color: #151210; + + /* warm grays */ + --ifm-font-color-base: #aaa19d; + --ifm-heading-color: #f4f1ef; + --ifm-color-content-secondary: #817a76; + + /* warm hairlines */ + --ifm-color-emphasis-200: #221e1c; + --ifm-color-emphasis-300: #2d2724; + --ifm-toc-border-color: #221e1c; + --ifm-table-border-color: #221e1c; + + /* code */ + --ifm-code-background: #1c1715; + --ifm-pre-background: #0a0908; + + --docusaurus-highlighted-code-line-bg: rgba(0, 175, 209, 0.14); + --echo-glow: 0 0 0 4px rgba(0, 175, 209, 0.10); +} + +/* atmosphere: faint warm grain + a single cyan glow up top */ +[data-theme='dark'] body::before { + content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; + background: radial-gradient(820px 460px at 84% -12%, rgba(0, 175, 209, 0.10), transparent 62%); +} +[data-theme='dark'] body::after { + content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0.025; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } +#__docusaurus { position: relative; z-index: 1; } +/* ====================== Headings (heavy sans) ====================== */ +.markdown h1, h1 { font-size: 1.95rem; letter-spacing: -0.02em; line-height: 1.1; } +.markdown h2 { font-size: 1.4rem; letter-spacing: -0.01em; margin-top: 2.8rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--ifm-color-emphasis-200); } +.markdown h3 { font-size: 1.12rem; letter-spacing: -0.01em; margin-top: 1.8rem; } +.markdown > p, .markdown li { font-size: 0.92rem; } + +/* ====================== Navbar ====================== */ +.navbar { + backdrop-filter: saturate(140%) blur(14px); + border-bottom: 1px solid var(--ifm-color-emphasis-200); + box-shadow: none; height: 60px; +} +.navbar__items { gap: 2px; } +.navbar__link { font-size: 13px; font-weight: 500; color: var(--ifm-color-content-secondary); padding: 6px 12px; border-radius: 7px; } +.navbar__link:hover, .navbar__link--active { color: var(--ifm-heading-color); background: var(--ifm-background-surface-color); } +.navbar__brand { margin-right: 1.2rem; } + +/* DocSearch button — precise, mono, ⌘K */ +.DocSearch-Button { + border-radius: 8px !important; + border: 1px solid var(--ifm-color-emphasis-300) !important; + background: var(--ifm-background-surface-color) !important; + font-family: var(--ifm-font-family-monospace) !important; + height: 36px; min-width: 200px; +} +.DocSearch-Button:hover { box-shadow: var(--echo-glow) !important; border-color: var(--ifm-color-primary) !important; } +.DocSearch-Button-Keys { min-width: auto !important; } + +/* ====================== Sidebar ====================== */ +.menu { font-size: 13.5px; padding: 18px 12px !important; } +.menu__list-item-collapsible .menu__link { font-weight: 600; } +.menu__link { border-radius: 7px; padding: 6px 11px; transition: background .14s, color .14s; color: var(--ifm-color-content-secondary); } +.menu__link:hover { background: var(--ifm-background-surface-color); color: var(--ifm-heading-color); } +.menu__link--active:not(.menu__link--sublist) { + background: var(--echo-accent-soft, rgba(0,137,164,.08)); + color: var(--ifm-color-primary); font-weight: 600; + box-shadow: inset 2px 0 0 var(--ifm-color-primary); +} +/* section captions: uppercase mono — terminal feel */ +.theme-doc-sidebar-item-category > .menu__list-item-collapsible > .menu__link, +.menu__list .menu__list-item-collapsible .menu__link--sublist { + text-transform: uppercase; letter-spacing: 0.08em; font-size: 11px; color: var(--ifm-color-content-secondary); +} +.theme-doc-sidebar-container { border-right: 1px solid var(--ifm-color-emphasis-200) !important; } + +/* ====================== Code ====================== */ +.theme-code-block, pre[class*="language-"] { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: var(--ifm-global-radius); + font-size: 13px; +} +code { border: 1px solid var(--ifm-color-emphasis-200); border-radius: 5px; padding: 1px 5px; } +.theme-code-block code { border: 0; padding: 0; } +[data-theme='dark'] .token.comment { color: #5f5853; } + +/* ====================== TOC ====================== */ +.table-of-contents { font-size: 12.5px; } +.table-of-contents__link--active { color: var(--ifm-color-primary); font-weight: 600; } +.theme-doc-toc-desktop { border-left: 1px solid var(--ifm-color-emphasis-200); } + +/* ====================== Cards / links ====================== */ +.card { + border: 1px solid var(--ifm-color-emphasis-200); + background: var(--ifm-card-background-color); + transition: transform .18s, border-color .18s, box-shadow .18s; +} +[data-theme='dark'] .card:hover { + border-color: var(--ifm-color-primary); transform: translateY(-2px); + box-shadow: 0 14px 38px -24px var(--echo-accent, #00afd1); +} +.breadcrumbs__item--active .breadcrumbs__link { color: var(--ifm-color-primary); background: var(--echo-accent-soft); } +article a { text-underline-offset: 3px; } + +/* scrollbar */ +[data-theme='dark'] ::-webkit-scrollbar { width: 10px; height: 10px; } +[data-theme='dark'] ::-webkit-scrollbar-thumb { background: #2d2724; border-radius: 8px; } +[data-theme='dark'] ::-webkit-scrollbar-thumb:hover { background: #3a322e; } + +/* ====================== Doc action toolbar ====================== */ +.doc-actions { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 24px; align-items: center; } +.doc-act { + display: inline-flex; align-items: center; gap: 6px; + border: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-surface-color); + color: var(--ifm-color-content-secondary); + border-radius: 7px; padding: 5px 11px; font-size: 12px; font-weight: 500; + font-family: var(--ifm-font-family-monospace); + cursor: pointer; transition: all .15s; line-height: 1; text-decoration: none; +} +.doc-act:hover { color: var(--ifm-heading-color); border-color: var(--ifm-color-primary); text-decoration: none; } +.doc-act .ph { font-size: 14px; } +.doc-act--primary { background: var(--echo-accent-soft); border-color: transparent; color: var(--ifm-color-primary); } +.doc-act--primary:hover { color: var(--ifm-color-primary); box-shadow: var(--echo-glow); } + +/* ====================== Ask Echo ⌘K palette ====================== */ +.ask-overlay { + position: fixed; inset: 0; z-index: 1000; + background: rgba(6, 5, 5, 0.66); backdrop-filter: blur(6px); + display: flex; align-items: flex-start; justify-content: center; padding-top: 11vh; + animation: askFade .15s ease; +} +@keyframes askFade { from { opacity: 0; } } +.ask-palette { + width: min(660px, 92vw); + background: var(--ifm-background-surface-color, #151210); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 16px; overflow: hidden; + box-shadow: 0 40px 120px -30px rgba(0,0,0,.9); + animation: askRise .2s cubic-bezier(.2,.8,.2,1); +} +@keyframes askRise { from { opacity: 0; transform: translateY(12px) scale(.98); } } +.ask-top { display: flex; align-items: center; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--ifm-color-emphasis-200); } +.ask-spark { color: var(--ifm-color-primary); font-size: 19px; } +.ask-input { flex: 1; background: none; border: 0; outline: none; color: var(--ifm-heading-color); font-size: 15px; font-family: var(--ifm-font-family-monospace); } +.ask-input::placeholder { color: var(--ifm-color-content-secondary); } +.ask-esc { font-family: var(--ifm-font-family-monospace); font-size: 11px; color: var(--ifm-color-content-secondary); border: 1px solid var(--ifm-color-emphasis-300); border-radius: 6px; padding: 3px 7px; } +.ask-body { max-height: 54vh; overflow-y: auto; padding: 12px 16px 16px; } +.ask-suggest { display: flex; flex-direction: column; gap: 2px; } +.ask-s { display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; padding: 10px 11px; border: 0; background: none; border-radius: 8px; color: var(--ifm-color-content-secondary); font-size: 13px; cursor: pointer; font-family: var(--ifm-font-family-monospace); transition: .14s; } +.ask-s:hover { background: var(--ifm-color-emphasis-200); color: var(--ifm-heading-color); } +.ask-s .ph { font-size: 17px; color: var(--ifm-color-primary); } +.ask-s .ask-k { margin-left: auto; font-size: 11px; opacity: .6; } +.ask-badge { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; color: var(--ifm-color-primary); background: var(--echo-accent-soft, rgba(0,175,209,.12)); border-radius: 20px; padding: 4px 11px; margin-bottom: 13px; font-weight: 600; letter-spacing: .02em; } +.ask-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ifm-color-primary); animation: askBlink 1.2s infinite; } +@keyframes askBlink { 50% { opacity: .3; } } +.ask-answer { font-size: 13.5px; line-height: 1.7; color: var(--ifm-font-color-base); } +.ask-answer strong { color: var(--ifm-heading-color); } +.ask-answer code { padding: 1px 5px; font-size: .9em; } +.ask-answer pre { margin: 13px 0; background: var(--ifm-pre-background, #0a0908); border: 1px solid var(--ifm-color-emphasis-200); border-radius: 9px; padding: 15px 16px; overflow-x: auto; font-size: 12.5px; line-height: 1.7; } +.ask-cursor { display: inline-block; width: 7px; height: 15px; background: var(--ifm-color-primary); vertical-align: -2px; margin-left: 2px; animation: askBlink .9s infinite; } +.ask-sources { margin-top: 16px; border-top: 1px solid var(--ifm-color-emphasis-200); padding-top: 13px; } +.ask-sources h5 { font-size: 10.5px; text-transform: uppercase; letter-spacing: .1em; color: var(--ifm-color-content-secondary); margin-bottom: 8px; } +.ask-src { display: flex; align-items: center; gap: 9px; padding: 7px 9px; border-radius: 7px; color: var(--ifm-color-content-secondary); font-size: 12.5px; } +.ask-src:hover { background: var(--ifm-color-emphasis-200); color: var(--ifm-heading-color); } +.ask-n { font-size: 11px; color: var(--ifm-color-primary); border: 1px solid var(--ifm-color-emphasis-300); border-radius: 5px; padding: 1px 6px; } +.ask-foot { display: flex; gap: 16px; align-items: center; border-top: 1px solid var(--ifm-color-emphasis-200); padding: 10px 18px; font-size: 11px; color: var(--ifm-color-content-secondary); } +.ask-foot b { color: var(--ifm-font-color-base); font-weight: 600; } + +/* Floating Ask Echo launcher (OpenClaw "Ask Molty" equivalent) */ +.ask-fab { + position: fixed; right: 22px; bottom: 22px; z-index: 900; + display: inline-flex; align-items: center; gap: 8px; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + color: var(--ifm-heading-color); + border-radius: 30px; padding: 9px 16px; + font-family: var(--ifm-font-family-monospace); font-size: 13px; cursor: pointer; + box-shadow: 0 12px 34px -12px rgba(0,0,0,.7); transition: all .16s; +} +.ask-fab:hover { border-color: var(--ifm-color-primary); transform: translateY(-1px); box-shadow: 0 14px 38px -12px rgba(0,175,209,.45); } +.ask-fab .ph { color: var(--ifm-color-primary); font-size: 16px; } +.ask-fab kbd { background: var(--ifm-color-emphasis-200); border: 1px solid var(--ifm-color-emphasis-300); border-radius: 5px; padding: 1px 6px; font-size: 11px; color: var(--ifm-color-content-secondary); } diff --git a/website/src/theme/DocItem/Content/index.js b/website/src/theme/DocItem/Content/index.js new file mode 100644 index 00000000..43c672e4 --- /dev/null +++ b/website/src/theme/DocItem/Content/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import Content from '@theme-original/DocItem/Content'; +import DocActions from '@site/src/components/DocActions'; + +// Wrap the doc body to render the Ask Echo / AI action toolbar above the content. +export default function ContentWrapper(props) { + return ( + <> + <DocActions /> + <Content {...props} /> + </> + ); +} diff --git a/website/src/theme/Root.js b/website/src/theme/Root.js new file mode 100644 index 00000000..dddc15e4 --- /dev/null +++ b/website/src/theme/Root.js @@ -0,0 +1,15 @@ +import React from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +// Root wraps the whole app — mount the global Ask Echo command palette (⌘K) here. +export default function Root({children}) { + return ( + <> + {children} + <BrowserOnly>{() => { + const AskEcho = require('@site/src/components/AskEcho').default; + return <AskEcho />; + }}</BrowserOnly> + </> + ); +} diff --git a/website/static/echo-redesign.html b/website/static/echo-redesign.html new file mode 100644 index 00000000..15c82225 --- /dev/null +++ b/website/static/echo-redesign.html @@ -0,0 +1,361 @@ +<!DOCTYPE html> +<html lang="en" data-theme="dark"> +<head> +<meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1.0" /> +<title>Echo — High performance, minimalist Go web framework + + + + + + + + + + + + +
+ + +
+ +

Quickstart

+

A high performance, minimalist Go web framework. Build a production-ready API in under five minutes.

+ +
+ + + Open in ChatGPT + Open in Claude + Edit +
+ +

Echo is built for speed and developer happiness. Its optimized HTTP router has zero dynamic memory allocation, smart route prioritization, and a clean, expressive API. Start with a single file:

+ +
+ go +
package main
+
+import (
+	"net/http"
+	"github.com/labstack/echo/v5"
+	"github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+	e := echo.New()
+
+	e.Use(middleware.Logger())
+	e.Use(middleware.Recover())
+
+	e.GET("/", func(c *echo.Context) error {
+		return c.JSON(http.StatusOK, map[string]string{
+			"message": "Hello, World!",
+		})
+	})
+
+	e.Logger.Fatal(e.Start(":1323"))
+}
+
+ +
+ +
Not sure where to start? Hit ⌘K and ask "How do I add JWT auth?" — Ask Echo answers from the docs, in your language.
+
+ +

Run it

+

Fetch dependencies and start the server. Echo requires Go 1.23+.

+
+ bash +
go mod init myapp
+go get github.com/labstack/echo/v5
+go run main.go
+
+

Your server is live at http://localhost:1323. Echo handles millions of requests per second per core with zero router allocations.

+ +

Next steps

+
+

Routing

Static, parameterized, and wildcard routes on a radix-tree router.

Explore
+

Middleware

50+ built-in middlewares: CORS, JWT, rate-limit, gzip, and more.

Explore
+

Data Binding

Bind JSON, XML, form, query & path params into typed structs.

Explore
+

Cookbook

Copy-paste recipes for auth, WebSocket, uploads, and deploys.

Explore
+
+
+ + +
+ +
+ +
+ + + + diff --git a/website/static/img/echo-social-card.png b/website/static/img/echo-social-card.png new file mode 100644 index 00000000..891bcdca Binary files /dev/null and b/website/static/img/echo-social-card.png differ diff --git a/website/static/img/favicon.svg b/website/static/img/favicon.svg new file mode 100644 index 00000000..b05b21d6 --- /dev/null +++ b/website/static/img/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/website/static/img/logo-dark.svg b/website/static/img/logo-dark.svg index 6de51c6a..f1c09baa 100644 --- a/website/static/img/logo-dark.svg +++ b/website/static/img/logo-dark.svg @@ -1,186 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/website/static/img/logo-light.svg b/website/static/img/logo-light.svg index 77521729..429e0363 100644 --- a/website/static/img/logo-light.svg +++ b/website/static/img/logo-light.svg @@ -1,59 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +