diff --git a/.gitattributes b/.gitattributes index 2f41707..8425c58 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -*.ipynb filter=strip-notebook-output +*.ipynb filter=strip-notebook-output diff --git a/.github/workflows/ci_pipeline.yml b/.github/workflows/ci_pipeline.yml index d3a6406..219ef20 100644 --- a/.github/workflows/ci_pipeline.yml +++ b/.github/workflows/ci_pipeline.yml @@ -44,7 +44,7 @@ jobs: run: | coverage run -m pytest . coverage report --sort=cover - + - name: Test tutorials run: | jupyter nbconvert --to notebook --execute tutorials/*.ipynb \ diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a9f6c1..0c27cd3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,15 @@ -name: Deploy docs to GitHub Pages +name: Docs + +# The Astro + Starlight site in docs/: hand-written pages, the executed tutorials and the API +# reference. Built on every push and pull request with the shared workflow of +# max-models/starlight-theme; deployed to GitHub Pages from devel. +# The TikZ tutorials compile their figures with pdflatex and pgfplots, hence the LaTeX packages. on: push: - branches: ["devel", "main"] # TODO: Set to main only after release + branches: ["main", "devel"] + pull_request: + branches: ["main", "devel"] workflow_dispatch: permissions: @@ -11,49 +18,32 @@ permissions: id-token: write concurrency: - group: "pages" - cancel-in-progress: false + group: docs-${{ github.ref }} + cancel-in-progress: true jobs: - build-and-deploy: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install pandoc and LaTeX - run: | - sudo apt-get update - sudo apt-get install -y pandoc - # pdflatex with pgfplots: the tutorials compile tikzfigure figures - sudo apt-get install -y --no-install-recommends \ - texlive-latex-base texlive-latex-extra texlive-pictures \ - texlive-fonts-recommended lmodern - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - # tikzfigure 0.4.0 is not on PyPI yet: install it from GitHub (drop once released) - pip install "tikzfigure[vis] @ git+https://github.com/max-models/tikzfigure@c290f43ac1deb4a4ed3b5b40919f10d3a6b53e8b" - pip install ".[docs]" - - - name: Build Sphinx docs - run: | - cd docs - make html - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload built docs - uses: actions/upload-pages-artifact@v3 - with: - path: docs/build/html/ - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + docs: + uses: max-models/starlight-theme/.github/workflows/docs.yml@v0 + with: + apt-packages: "texlive-latex-base texlive-latex-extra texlive-pictures texlive-fonts-recommended lmodern" + # the example web figures of guides/web-figures, from scripts/web_figure_examples.py + pre-build: '"$DOCS_PYTHON" scripts/web_figure_examples.py' + # stable-branch: main # deploy the latest release at // and devel at //dev/ + # a notebook's outputs are numbered in order: in tutorial 07, 1.html is the Plotly + # preview and 2.png the first compiled TikZ figure + smoke-paths: | + getting-started/installation/index.html + getting-started/quickstart/index.html + guides/matplotlib-import/index.html + guides/web-figures/index.html + figures/waves.plotly.json + figures/heatmap.plotly.json + figures/packet.plotly.json + tutorials/index.html + tutorials/tutorial-01/index.html + tutorials/tutorial-07-tikz/index.html + tutorials/tutorial-17-xarray/index.html + tutorials/tutorial-07-tikz/2.png + tutorials/tutorial-01/tutorial_01.ipynb + api/maxplotlib/index.html + api/maxplotlib/canvas/index.html diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eaff1cd..2e65b3f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,4 +31,3 @@ jobs: TWINE_USERNAME: __token__ # Use API token TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: twine upload dist/* - diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index e3fa1e5..acdd099 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -24,57 +24,6 @@ jobs: ./cloc --version ./cloc $(git ls-files) - black: - runs-on: ubuntu-latest - steps: - - name: Checkout the code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Code formatting with black - run: | - pip install "black[jupyter]" - black --check src/ - black --check tutorials/ - - isort: - runs-on: ubuntu-latest - steps: - - name: Checkout the code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Code formatting with isort - run: | - pip install isort - isort --check src/ - isort --check tutorials/ - - mypy: - runs-on: ubuntu-latest - steps: - - name: Checkout the code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Type check with mypy - continue-on-error: true - run: | - pip install mypy - mypy src/ - prospector: runs-on: ubuntu-latest steps: @@ -103,11 +52,18 @@ jobs: with: python-version: "3.11" - - name: Lint with ruff - continue-on-error: true + - name: Linting with ruff (including import sorting) run: | pip install ruff - ruff check src/ + ruff check . + + - name: Code formatting with ruff + run: ruff format --check . + + - name: Check statement order + run: | + pip install ssort + ssort --check --diff . pylint: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 515f6e9..0f3a2b3 100644 --- a/.gitignore +++ b/.gitignore @@ -68,8 +68,6 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ # PyBuilder .pybuilder/ @@ -172,6 +170,11 @@ figures/ *.mp4 *.pkl *.json +# except the docs site and Renovate config +!docs/package.json +!docs/package-lock.json +!docs/tsconfig.json +!renovate.json # tex *.aux @@ -197,3 +200,5 @@ docs/superpowers/ docs/tutorials/ tutorials/*.txt tutorials/*.png +tutorials/*.tikz +tutorials/*.html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af67085..2effd49 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,27 @@ +--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 # Use the latest stable version + rev: v6.0.0 hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace - id: check-added-large-files # Prevent giant files from being committed. - args: ["--maxkb=1000"] - - id: check-merge-conflict # Check for files that contain merge conflict strings. - - id: check-toml # Attempts to load all TOML files to verify syntax. - - id: check-yaml # Attempts to load all yaml files to verify syntax. - args: ["--unsafe"] - + args: [--maxkb=500] + exclude: ^(uv\.lock|docs/package-lock\.json)$ # generated lock files + - id: check-merge-conflict # Check for files that contain merge conflict strings. - repo: https://github.com/kynan/nbstripout - rev: 0.8.1 + rev: 0.9.1 hooks: - - id: nbstripout # remove jupyter notebook cell output + - id: nbstripout # strip jupyter notebook output + - repo: https://github.com/bwhmather/ssort + rev: 0.17.0 + hooks: + - id: ssort # order statements so names are defined before they are used + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.17.3 + hooks: + - id: ruff-check # lint, including import sorting (the I rules) + args: [--fix] + - id: ruff-format diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 675e1e2..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Max - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..45bdce5 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +# The documentation site (docs/, Astro + Starlight). +# Notebook tool: starlight-notebooks, from @max-models/starlight-theme (docs/node_modules). +PYTHON ?= .venv/bin/python +DOCS_ENV = DOCS_PYTHON=$(abspath $(PYTHON)) +NOTEBOOKS = cd docs && $(DOCS_ENV) npx starlight-notebooks + +.PHONY: help docs-install docs-notebooks docs-dev docs-build docs-preview docs-clean + +help: ## list the targets + @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-16s %s\n", $$1, $$2}' + +docs-install: ## Python docs extra and the npm packages + $(PYTHON) -m pip install -e ".[docs]" + cd docs && npm ci + +docs-notebooks: ## execute the changed tutorials and convert all of them (FORCE=1 reruns all) + $(NOTEBOOKS) execute $(if $(FORCE),--force,) + $(NOTEBOOKS) convert + +docs-dev: ## live preview at http://localhost:4321/maxplotlib/ + $(NOTEBOOKS) convert + cd docs && $(DOCS_ENV) DOCS_VALIDATE_LINKS=false npm run dev + +docs-build: ## the static site in docs/dist, as in CI + $(NOTEBOOKS) convert + cd docs && npm run check:mdx && $(DOCS_ENV) npm run build + +docs-preview: ## serve docs/dist + cd docs && npm run preview + +docs-clean: ## remove generated pages, assets and build output + $(NOTEBOOKS) clean + rm -rf docs/dist docs/.astro diff --git a/README.md b/README.md index 11f4beb..8961f24 100644 --- a/README.md +++ b/README.md @@ -78,18 +78,20 @@ providing access to any Matplotlib API without requiring a maxplotlib wrapper: ``` python -canvas.plot(matplotlib_customizations={ - "figure": { - "suptitle": "My figure", - }, - "axes": { - "tick_params": { - "axis": "both", - "which": "major", - "length": 6, +canvas.plot( + matplotlib_customizations={ + "figure": { + "suptitle": "My figure", }, - }, -}) + "axes": { + "tick_params": { + "axis": "both", + "which": "major", + "length": 6, + }, + }, + } +) ``` For dynamic customization, the same option also accepts a function: @@ -100,6 +102,7 @@ def customize(fig, axes): for ax in axes.flat: ax.tick_params(axis="both", which="major", length=6) + canvas.plot(matplotlib_customizations=customize) ``` @@ -188,7 +191,7 @@ print(terminal_fig.build(keep_colors=False)) ``` Trigonometry - Runtime + Runtime ┌┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┐ 1.00┼ ▞▞ sin(x) ──▗▄▞▀▀▀▀▀▙▄▖────────────┼─────────────────┼─────────────▄▄▀▀▀┤ │ ▞▞ cos(x) ▄▀▘ │ ▝▀▄ │ │ ▄▞▀ ││ @@ -208,8 +211,8 @@ print(terminal_fig.build(keep_colors=False)) ││ │ ▀▄▖ │ ▗▄▀ ▀▄▖ │ ▗▄▀▘ ││ -1.00┼┼─────────────────┼────────────▝▀▚▄▄▄▄▄▞▀▘───────▝▀▜▄▄▄▄▄▞▀▘────────────┼┤ └┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼┘ - 0.0 1.6 3.1 4.7 6.3 - Duration Time + 0.0 1.6 3.1 4.7 6.3 + Duration Time Or plot with the TikZ backend: @@ -283,7 +286,7 @@ ax.set_legend(True) canvas.show(backend="plotext") ``` - Terminal plot + Terminal plot ┌──────────────────────────────────────────────────────────────────────────┐ 3.16┤ ▞▞ sqrt(x) ▄▞│ │ │▗▄▞▀ │ @@ -303,8 +306,8 @@ canvas.show(backend="plotext") │┼ ▗▄▄▞▀▀▀▀▀ │ 0.93┤│▀▀▘ │ └┬─────────────────┬──────────────────┬─────────────────┬─────────────────┬┘ - 1.0 1.8 3.2 5.6 10.0 - y x + 1.0 1.8 3.2 5.6 10.0 + y x @@ -383,7 +386,9 @@ wave = xr.DataArray( attrs={"long_name": "Potential", "units": "V"}, ) -wave.maxplot.pcolormesh(col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6}).show() +wave.maxplot.pcolormesh( + col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6} +).show() ``` ![](README_files/figure-commonmark/cell-20-output-1.png) diff --git a/README.qmd b/README.qmd index 13111d0..527e7d8 100644 --- a/README.qmd +++ b/README.qmd @@ -77,18 +77,20 @@ any Matplotlib API without requiring a maxplotlib wrapper: ```{python} #| output: false -canvas.plot(matplotlib_customizations={ - "figure": { - "suptitle": "My figure", - }, - "axes": { - "tick_params": { - "axis": "both", - "which": "major", - "length": 6, +canvas.plot( + matplotlib_customizations={ + "figure": { + "suptitle": "My figure", }, - }, -}) + "axes": { + "tick_params": { + "axis": "both", + "which": "major", + "length": 6, + }, + }, + } +) ``` For dynamic customization, the same option also accepts a function: @@ -100,6 +102,7 @@ def customize(fig, axes): for ax in axes.flat: ax.tick_params(axis="both", which="major", length=6) + canvas.plot(matplotlib_customizations=customize) ``` @@ -310,7 +313,9 @@ wave = xr.DataArray( attrs={"long_name": "Potential", "units": "V"}, ) -wave.maxplot.pcolormesh(col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6}).show() +wave.maxplot.pcolormesh( + col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6} +).show() ``` The same works through Canvas methods, e.g. `canvas.plot(da, hue="species")`, diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..2a97a9b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,8 @@ +# Astro +dist/ +.astro/ +node_modules/ +# generated by starlight-notebooks (regenerated in CI) +.cache/ +public/tutorials/ +src/content/docs/tutorials/*.mdx diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 0000000..dad6255 --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,56 @@ +// @ts-check +// The shared max-models docs setup (@max-models/starlight-theme): served at +// https://max-models.github.io/maxplotlib/, with math, the API reference and a link check. +import { defineDocs } from '@max-models/starlight-theme'; + +// The tutorial pages of the old Sphinx site (tutorials/tutorial_01.html, ...) +const tutorials = [ + 'tutorial_01', + 'tutorial_02', + 'tutorial_03', + 'tutorial_04', + 'tutorial_05', + 'tutorial_06', + 'tutorial_07_tikz', + 'tutorial_08_plotly', + 'tutorial_09_plotext', + 'tutorial_10_matplotlib_nxm_spacing', + 'tutorial_11_gantt_charts', + 'tutorial_12_flame_charts', + 'tutorial_13_advanced_matplotlib', + 'tutorial_14_axis_and_layout_controls', + 'tutorial_15_tikzfigure_subplots', + 'tutorial_16_plotext_advanced', + 'tutorial_17_xarray', +]; + +export default defineDocs({ + repo: 'maxplotlib', + title: 'maxplotlib', + description: 'A reproducible plotting module with Matplotlib, Plotly, plotext and TikZ backends.', + pythonPackage: 'maxplotlib', + nav: [ + { label: 'Getting started', link: '/getting-started/installation/' }, + { label: 'Tutorials', link: '/tutorials/' }, + { label: 'Guides', link: '/guides/matplotlib-import/' }, + { label: 'Development', link: '/development/docs/' }, + { label: 'API', link: '/api/maxplotlib/', section: '/api/' }, + ], + sidebar: [ + { + label: 'Getting started', + items: [ + { label: 'Installation', slug: 'getting-started/installation' }, + { label: 'Quickstart', slug: 'getting-started/quickstart' }, + ], + }, + { label: 'Tutorials', items: [{ autogenerate: { directory: 'tutorials' } }] }, + { label: 'Guides', items: [{ autogenerate: { directory: 'guides' } }] }, + { label: 'Development', items: [{ label: 'Building the docs', slug: 'development/docs' }] }, + ], + astro: { + redirects: Object.fromEntries( + tutorials.map((name) => [`/tutorials/${name}.html`, `/tutorials/${name.replaceAll('_', '-')}/`]) + ), + }, +}); diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 747ffb7..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..5894b38 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,6749 @@ +{ + "name": "maxplotlib-docs", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maxplotlib-docs", + "version": "0.0.1", + "dependencies": { + "@astrojs/starlight": "^0.42.6", + "@max-models/starlight-theme": "github:max-models/starlight-theme#v0", + "astro": "^7.4.0", + "sharp": "^0.35.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.2.tgz", + "integrity": "sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-x64": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.2", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.2", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.2", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.2", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.2" + } + }, + "node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.2.tgz", + "integrity": "sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.2.tgz", + "integrity": "sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.2.tgz", + "integrity": "sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.2.tgz", + "integrity": "sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.2.tgz", + "integrity": "sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.2.tgz", + "integrity": "sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.2.tgz", + "integrity": "sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.2.tgz", + "integrity": "sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.2.tgz", + "integrity": "sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.2.tgz", + "integrity": "sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.2.tgz", + "integrity": "sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.5.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.13.0.tgz", + "integrity": "sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "picomatch": "^4.0.7", + "retext-smartypants": "^6.2.0", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "unified": "^11.0.5", + "yaml": "^2.9.1" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.3.3.tgz", + "integrity": "sha512-yC7XCJtRztiR3GiP8e0WwUAGJeKm5goMDDzcZykfbOOpQUDztBWG66fBeIOfl1uVxvhdI0iLzkeaHgqPhaRfUg==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.0", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.19.0", + "estree-util-visit": "^2.0.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-html": "^9.0.5", + "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.3", + "source-map": "^0.8.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.4.tgz", + "integrity": "sha512-Byhe9CAiGFJrhMACKpgN/FPf4lcwapqwFQAB7E1sPELTEJINfBIpgnEYOBTXzr//fvSSNhfp/X3AqN4mUV9p+A==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.0", + "github-slugger": "^2.0.0", + "satteri": "^0.10.5" + } + }, + "node_modules/@astrojs/mdx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.1.0.tgz", + "integrity": "sha512-xzrlXrjpBYvC27MTkP+2NMiJG4Jjl2DcoNc/EF9RNxAp0dwhvXC3b22+7OO4kHJQKfISk2uy38HYp1UxX7JfSg==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/markdown-satteri": "0.4.4", + "es-module-lexer": "^3.0.3" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "@astrojs/markdown-satteri": "^0.4.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/prism": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.0.tgz", + "integrity": "sha512-zvRaL3MKTAXWfVQrHAlYtocRZWXBM6ruF3Bl7Lc7QgFHLRTfwMKo33KczlkvkXDY+qcytKhAWOT9dwUUYLUxAw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.22.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", + "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.42.6", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.6.tgz", + "integrity": "sha512-Cfs7GdgzgNRCauDxQVsFUVL1thrGHK/rQdbv/SURv/xhGBXx+j5ypOHW9ENPraQxbIzUC8KAnJV7L9mvEVRuyQ==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.0", + "@astrojs/mdx": "^8.0.0", + "@astrojs/sitemap": "^3.7.3", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.5", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.44.0", + "bcp-47": "^2.1.0", + "hast-util-format": "^1.1.0", + "hast-util-select": "^6.0.4", + "hast-util-to-html": "^9.0.5", + "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": "^1.2.3", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "remark-directive": "^4.0.0", + "satteri": "^0.10.3", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.4.0.tgz", + "integrity": "sha512-Qt2T6RGtYacyV8OZU3IcyFbsp69s6qIcnWvQvn7/p9MKebPCpEswP5Ji4RhifG3DfeBX7e07Pjj5smkd+Tfp7Q==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "package-manager-detector": "^1.9.0" + }, + "engines": { + "node": ">=22.22.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.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/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/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.5.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.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", + "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", + "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.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", + "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", + "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", + "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "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.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "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.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "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.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "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.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "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.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@max-models/starlight-theme": { + "version": "0.7.0", + "resolved": "git+ssh://git@github.com/max-models/starlight-theme.git#35ba7be44b183dd82e92f638a46d095a3fbaa23d", + "license": "MIT", + "workspaces": [ + "example/docs" + ], + "dependencies": { + "@astrojs/markdown-remark": "^7.3.2", + "@mdx-js/mdx": "^3.1.1", + "katex": "^0.19.0", + "plotly.js-dist-min": "^4.1.2", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "sharp": "^0.35.3", + "starlight-links-validator": "^0.27.0", + "starlight-pydocs": "0.3.0", + "yaml": "^2.9.1" + }, + "bin": { + "starlight-check-mdx": "bin/check-mdx.mjs", + "starlight-gallery": "bin/gallery.mjs", + "starlight-notebooks": "bin/notebooks.mjs", + "starlight-theme-init": "bin/init.mjs" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.42.6", + "astro": ">=7.3.8" + } + }, + "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/@mdx-js/mdx/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/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "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/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "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/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz", + "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "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/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "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.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "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/am-i-vibing": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.5.0.tgz", + "integrity": "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "7.4.1", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.4.1.tgz", + "integrity": "sha512-mzWNEWSef1VQDgFku6Gp0v8Qx53NOkNlOIDQcX9ovxAZ1QXic6cks/FFo+gxPM9anXuat4X1FhB61KTk3XFvgg==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-rs": "^0.5.1", + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/markdown-satteri": "0.4.4", + "@astrojs/telemetry": "3.4.0", + "@capsizecss/unpack": "^4.0.1", + "@clack/prompts": "^1.8.1", + "@oslojs/encoding": "^1.1.0", + "am-i-vibing": "^0.5.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": "^2.0.1", + "devalue": "^6.0.2", + "diff": "^9.0.0", + "dset": "^3.1.4", + "es-module-lexer": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.6", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.3.0", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.4.3", + "magicast": "^0.5.5", + "mrmime": "^2.0.1", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.3", + "p-queue": "^9.3.5", + "package-manager-detector": "^1.9.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.7", + "rolldown": "^1.2.13", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "svgo": "^4.1.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "tinyps": "^0.5.0", + "ultrahtml": "^1.7.0", + "unifont": "^1.0.3", + "unstorage": "^1.17.5", + "verkit": "^0.5.0", + "vite": "^8.3.4", + "vitefu": "^1.1.3", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.6.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.22.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/astro-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", + "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.44.2", + "url-extras": "^0.1.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", + "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": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.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": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "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": "6.0.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-6.0.3.tgz", + "integrity": "sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "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/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "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/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.1", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.1.tgz", + "integrity": "sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA==", + "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-to-js/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/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.44.2", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.2.tgz", + "integrity": "sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "@expressive-code/plugin-frames": "^0.44.2", + "@expressive-code/plugin-shiki": "^0.44.2", + "@expressive-code/plugin-text-markers": "^0.44.2" + } + }, + "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/fnv1a-64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fnv1a-64/-/fnv1a-64-0.1.2.tgz", + "integrity": "sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==", + "license": "MIT" + }, + "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.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.6.tgz", + "integrity": "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==", + "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/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.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-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-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.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "26.5.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.3.tgz", + "integrity": "sha512-6pRwOwJ6lBCBB66fqNZaSoUmvsALHMord7FL6GsDhubT1CrPabTMEJIqddm0nn2YEbKofdI/3VAtGrQPuztY1Q==", + "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 || ^7" + }, + "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-absolute-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-5.0.0.tgz", + "integrity": "sha512-sdJyNpBnQHuVnBunfzjAecOhZr2+A30ywfFvu3EnxtKLUWfwGgyWUmqHbGZiU6vTfHpCPm5GvLe4BAvlU9n8VQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "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-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/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "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/katex": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.19.0.tgz", + "integrity": "sha512-v6Tznz3zJ7u3niRCoDTsumM2+HA2XXcCu+WAacCeHD2z3p9A9Ks987o5FzfTGBN0e8A0vjEgIDvLbICrXpdw/Q==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^15.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "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.1", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.1.tgz", + "integrity": "sha512-Gqpa3MHorXYEZ0VfbroyvBlVKUfz371V6mIERqOYMcb0W1D3LwmFq1ZW4O2TDrbn+4ZmGLwMWeFRJgYZ+bc5fQ==", + "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", + "micromark-util-character": "^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.3", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz", + "integrity": "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==", + "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.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", + "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.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", + "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-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "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.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", + "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", + "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-character": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^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.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "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-edit-map": "^1.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.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "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.1.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-edit-map": "^1.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.3" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "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-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/micromark-extension-math/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "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-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "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-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.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "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.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", + "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.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "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/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.3", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", + "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", + "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.5", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.5.tgz", + "integrity": "sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==", + "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.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "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.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plotly.js-dist-min": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-4.1.2.tgz", + "integrity": "sha512-VSmS9KVNun16S8qFYYp9LlL/XBby4ueeGqB+K9AqRCVsup8dSpGULap4UyR1f2mz4pV3qjWp1sR5chwLgIKPoQ==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "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.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", + "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/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "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-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.2.tgz", + "integrity": "sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.44.2" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/rehype-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-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.3", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.3.tgz", + "integrity": "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==", + "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/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", + "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.5.0", + "@shikijs/engine-javascript": "4.5.0", + "@shikijs/engine-oniguruma": "4.5.0", + "@shikijs/langs": "4.5.0", + "@shikijs/themes": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "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.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "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/starlight-links-validator": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.27.0.tgz", + "integrity": "sha512-UvPl8L9hfWR+yWbLOEtljLmpJCGcM6AVseVy5IO9dmqdOjW4R+Y8FaYyVA5axCC8RvxM5xU7M1WTerpwU8rYbw==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "is-absolute-url": "^5.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdast-util-to-hast": "^13.2.1", + "picomatch": "^4.0.3", + "satteri": "^0.10.3", + "terminal-link": "^5.0.0", + "unist-util-visit": "^5.1.0", + "yaml": "^2.8.3" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.42.0", + "astro": ">=7.2.10" + } + }, + "node_modules/starlight-pydocs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/starlight-pydocs/-/starlight-pydocs-0.3.0.tgz", + "integrity": "sha512-2RVc8fhDvc9ECBt91W3G1OLXUqj0wNF4FCdk7qGj0v0zP6ngl+0jeH9C0cNbFDG/x1/E6r+VZyrAYciCDB8pcA==", + "license": "MIT", + "dependencies": { + "shiki": "^4.0.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.2.0", + "@astrojs/starlight": ">=0.41.0", + "astro": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + }, + "@astrojs/starlight": { + "optional": true + } + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "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/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-hyperlinks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.6.0.tgz", + "integrity": "sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==", + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/svgo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^6.0.0", + "css-tree": "^3.0.1", + "css-what": "^7.0.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "1.6.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/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/terminal-link": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-5.0.0.tgz", + "integrity": "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^4.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "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/tinyps": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinyps/-/tinyps-0.5.0.tgz", + "integrity": "sha512-Llu7nEtVOGHh7jok32l6+udlu+4Np5vsYVB7zeTK614coSsro2qjwte7MFt/ouLM/h4kXqbGXktSMMvMsttSDw==", + "license": "MIT" + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "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": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "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": "1.0.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-1.0.3.tgz", + "integrity": "sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "fnv1a-64": "^0.1.2", + "std-env": "^4.2.0", + "undici": "^8.0.0" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.6.tgz", + "integrity": "sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.11", + "lru-cache": "^11.5.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.4" + }, + "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 || ^11.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/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/verkit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "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": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/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/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "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.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "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/docs/package.json b/docs/package.json new file mode 100644 index 0000000..8fa4457 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,24 @@ +{ + "name": "maxplotlib-docs", + "private": true, + "type": "module", + "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check:mdx": "starlight-check-mdx", + "notebooks": "starlight-notebooks", + "astro": "astro" + }, + "dependencies": { + "@astrojs/starlight": "^0.42.6", + "@max-models/starlight-theme": "github:max-models/starlight-theme#v0", + "astro": "^7.4.0", + "sharp": "^0.35.3" + } +} diff --git a/docs/public/getting-started/quickstart-sine.png b/docs/public/getting-started/quickstart-sine.png new file mode 100644 index 0000000..7b43e83 Binary files /dev/null and b/docs/public/getting-started/quickstart-sine.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index b976a6d..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,51 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -import os -import shutil - - -def copy_tutorials(app): - src = os.path.abspath("../tutorials") - dst = os.path.abspath("source/tutorials") - - # Remove existing target directory if it exists - if os.path.exists(dst): - shutil.rmtree(dst) - - shutil.copytree(src, dst) - - -def setup(app): - app.connect("builder-inited", copy_tutorials) - - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = "maxplotlib" -copyright = "2025, Max Lindqvist" -author = "Max Lindqvist" - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - "nbsphinx", - "sphinx.ext.mathjax", - "sphinx.ext.autodoc", -] - -# Execute every tutorial notebook when building the documentation. -nbsphinx_execute = "always" - -templates_path = ["_templates"] - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 82a2734..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. maxplotlib documentation master file, created by - sphinx-quickstart on Sun Jun 22 11:14:00 2025. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -maxplotlib documentation -======================== - -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. - - -.. toctree:: - :maxdepth: 2 - :caption: Tutorials: - - tutorials/tutorial_01 - tutorials/tutorial_02 - tutorials/tutorial_03 - tutorials/tutorial_04 - tutorials/tutorial_05 - tutorials/tutorial_06 - tutorials/tutorial_07_tikz - tutorials/tutorial_08_plotly - tutorials/tutorial_09_plotext - tutorials/tutorial_10_matplotlib_nxm_spacing - tutorials/tutorial_11_gantt_charts - tutorials/tutorial_12_flame_charts - tutorials/tutorial_13_advanced_matplotlib - tutorials/tutorial_14_axis_and_layout_controls - tutorials/tutorial_15_tikzfigure_subplots - tutorials/tutorial_16_plotext_advanced - tutorials/tutorial_17_xarray diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts new file mode 100644 index 0000000..08161a2 --- /dev/null +++ b/docs/src/content.config.ts @@ -0,0 +1 @@ +export { collections } from '@max-models/starlight-theme/content'; diff --git a/docs/src/content/docs/development/docs.md b/docs/src/content/docs/development/docs.md new file mode 100644 index 0000000..0ae43d9 --- /dev/null +++ b/docs/src/content/docs/development/docs.md @@ -0,0 +1,55 @@ +--- +title: Building the docs +description: How this documentation site is built from Markdown pages, Jupyter tutorials and docstrings. +--- + +The site in `docs/` is built with [Astro](https://astro.build/) and +[Starlight](https://starlight.astro.build/). It has three kinds of pages: + +- **Hand-written pages** in `docs/src/content/docs/`, as Markdown or MDX. +- **Tutorials**, converted from the Jupyter notebooks in `tutorials/` by `starlight-notebooks`. + Each notebook is executed and its outputs (text, images, tables) are written into an MDX page. +- **API reference**, generated from the docstrings by + [starlight-pydocs](https://ewels.github.io/starlight-pydocs/), one page per module. + +The configuration (`defineDocs` in `docs/astro.config.mjs`), the look, the header, the notebook +tool and the build workflow come from the shared +[starlight-theme](https://github.com/max-models/starlight-theme) of the max-models projects; +change them there, and update the version in `docs/package.json` and +`.github/workflows/docs.yml` to pick up a new release. + +## Requirements + +- Node 22 or newer. +- The package installed with the `docs` extra: `pip install -e ".[docs]"`. +- LaTeX with PGF/TikZ and pgfplots (`pdflatex`): the TikZ tutorials compile their figures. + +## Commands + +```bash +make docs-install # npm packages and the Python docs extra +make docs-notebooks # execute tutorials/*.ipynb and convert them to pages +make docs-dev # live preview at http://localhost:4321/maxplotlib/ +make docs-build # the static site in docs/dist, as in CI +make docs-preview # serve docs/dist +make docs-clean # remove generated pages and build output +``` + +`make docs-notebooks` only reruns a notebook whose cells changed since the last run; pass +`FORCE=1` to rerun all of them. `PYTHON=...` picks the interpreter (default `.venv/bin/python`). + +## Writing tutorials + +Put a Jupyter notebook in `tutorials/`. Its first `# Heading` becomes the page title and the +first paragraph its description; the pages are ordered by file name, or by `docs_order` in the +notebook metadata. Cell tags control the output: + +| Tag | Effect | +| ------------------ | ---------------------------------------- | +| `remove-cell` | the cell is left out of the page | +| `hide-input` | the code is hidden, the outputs are kept | +| `fold-input` | the code is in a closed "Show code" box | +| `remove-output` | the outputs are left out | +| `raises-exception` | an error output is shown, not a failure | + +Math in `$...$` and `$$...$$` is rendered with KaTeX. diff --git a/docs/src/content/docs/getting-started/installation.md b/docs/src/content/docs/getting-started/installation.md new file mode 100644 index 0000000..bb0696b --- /dev/null +++ b/docs/src/content/docs/getting-started/installation.md @@ -0,0 +1,38 @@ +--- +title: Installation +description: Install maxplotlib from PyPI, with the optional xarray support and LaTeX for the TikZ backend. +--- + +maxplotlib is on PyPI as `maxplotlibx` (the import name is `maxplotlib`): + +```sh +pip install maxplotlibx +``` + +With the [xarray](https://docs.xarray.dev) accessor (`import maxplotlib.xarray`): + +```sh +pip install "maxplotlibx[xarray]" +``` + +Then, in Python: + +```python +from maxplotlib import Canvas +``` + +## Backends + +Matplotlib, Plotly, plotext and [tikzfigure](https://github.com/max-models/tikzfigure) are +installed with the package. The TikZ backend writes LaTeX code; to compile it into a PDF or PNG +(`canvas.savefig("figure.pdf", backend="tikzfigure")`, or the figures in the TikZ tutorials) you +also need a LaTeX distribution with `pdflatex` and PGF/TikZ with pgfplots, for example TeX Live +(`texlive-latex-extra texlive-pictures` on Debian/Ubuntu) or MacTeX. + +## From source + +```sh +git clone https://github.com/max-models/maxplotlib +cd maxplotlib +pip install -e ".[dev]" +``` diff --git a/docs/src/content/docs/getting-started/quickstart.md b/docs/src/content/docs/getting-started/quickstart.md new file mode 100644 index 0000000..3053a1e --- /dev/null +++ b/docs/src/content/docs/getting-started/quickstart.md @@ -0,0 +1,62 @@ +--- +title: Quickstart +description: Build a canvas, add data, and render it with the Matplotlib, Plotly, plotext or TikZ backend. +--- + +A `Canvas` holds the figure: its subplots, lines and styling, independent of how it is drawn. +The backend is chosen when you render it. + +```python +import numpy as np +from maxplotlib import Canvas + +x = np.linspace(0, 2 * np.pi, 200) +y = np.sin(x) + +canvas, ax = Canvas.subplots() +ax.plot(x, y) +``` + +Plot the figure with the default (Matplotlib) backend: + +```python +canvas.show() +``` + +![A sine curve drawn by the Matplotlib backend](/maxplotlib/getting-started/quickstart-sine.png) + +Use `canvas.plot(x, y)` to add data directly to a canvas. When you want to explicitly render an +already-built canvas, use `canvas.render(...)`. The older `canvas.plot(backend=...)` spelling is +still supported for compatibility, but emits a `FutureWarning`. + +Add several lines at once with shared styling: + +```python +canvas.plot_many( + [(x, np.sin(x)), (x, np.cos(x))], + labels=["sin(x)", "cos(x)"], + linewidth=2, +) +``` + +## Other backends + +The same canvas renders with every backend: + +```python +canvas.show(backend="plotly") # an interactive Plotly figure +canvas.show(backend="plotext") # a plot in the terminal +canvas.savefig("sine.pdf", backend="tikzfigure") # TikZ code, compiled with LaTeX +``` + +`savefig` picks the format from the file name: whatever Matplotlib writes (PNG, PDF, SVG, ...); +HTML for Plotly (`include_plotlyjs="cdn"` for a small file that loads plotly.js from a CDN), or +an image with [kaleido](https://github.com/plotly/Kaleido); a text file for plotext; and `.tikz`, +`.tex`, PDF or PNG for TikZ (PDF and PNG compiled with LaTeX). + +## Next + +The [tutorials](/maxplotlib/tutorials/) go through subplots, plot types, styling, axes and +annotations, layers, each backend in detail, Gantt and flame charts, and xarray data. An existing +Matplotlib figure can be imported with `Canvas.from_matplotlib`; see +[Matplotlib import](/maxplotlib/guides/matplotlib-import/). diff --git a/docs/matplotlib-import-support.md b/docs/src/content/docs/guides/matplotlib-import.md similarity index 98% rename from docs/matplotlib-import-support.md rename to docs/src/content/docs/guides/matplotlib-import.md index 5bef77d..6d98c9a 100644 --- a/docs/matplotlib-import-support.md +++ b/docs/src/content/docs/guides/matplotlib-import.md @@ -1,4 +1,7 @@ -# Matplotlib import support and roadmap +--- +title: Matplotlib import +description: What Canvas.from_matplotlib imports from a Matplotlib figure today, and what is planned. +--- `Canvas.from_matplotlib(source, strict=False, **canvas_kwargs)` accepts an in-memory Matplotlib figure, axes, or rectangular axes array. This checklist diff --git a/docs/src/content/docs/guides/web-figures.mdx b/docs/src/content/docs/guides/web-figures.mdx new file mode 100644 index 0000000..378e590 --- /dev/null +++ b/docs/src/content/docs/guides/web-figures.mdx @@ -0,0 +1,180 @@ +--- +title: Web figures +description: Save a canvas as theme-neutral Plotly JSON and embed it, interactive and in the site's colours, on a page of a max-models docs site. +--- + +import PlotlyFigure from '@max-models/starlight-theme/components/PlotlyFigure.astro'; + +A web figure is a Plotly figure saved as JSON, without plotly.js and without anything that +fixes its look to one page. A page of a max-models docs site embeds it with the theme's +`PlotlyFigure` component: the figure is interactive, plotly.js is loaded only when the figure +scrolls into view, and the figure takes the site's colours in light and dark mode. + +## Examples + +The figures below are web figures, embedded on this page with `PlotlyFigure`. Switch the site +between light and dark mode (top right) to see them follow it; hover, zoom and pan work as in +any Plotly figure. + +### Lines in the site's palette + +The four waves have no colour of their own, so they take the site's data palette in order. The +black reference line takes the text colour, so it stays visible in dark mode. + + + +```python +x = np.linspace(0, 4 * np.pi, 400) +canvas = Canvas(width="12cm", ratio=0.5) # the aspect ratio of the embedded figure: 2 +for k in range(4): + canvas.plot(x, np.exp(-0.1 * x) * np.sin(x - k * np.pi / 4), label=f"phase {k}/4 π") +canvas.plot(x, 0 * x, color="black", linewidth=0.8) +canvas.set_xlabel("t") +canvas.set_ylabel("amplitude") +canvas.set_legend(True) +canvas.savefig("docs/public/figures/waves.plotly.json", backend="plotly") +``` + +### Any Plotly figure: a colormap and a contour + +`write_web_figure` takes any Plotly figure. A colormap stays as it is; the contour drawn in +Matplotlib's `C1` (`#ff7f0e`) carries that position and takes the palette's second colour. + + + +```python +import plotly.graph_objects as go + +from maxplotlib.web import write_web_figure + +fig = go.Figure( + [ + # zsmooth: a smooth field instead of visible cells (keep the cells for grid data) + go.Heatmap(x=x, y=y, z=z, colorscale="Viridis", colorbar={"title": "φ"}, zsmooth="best"), + go.Contour( + x=x, y=y, z=z, + contours={"coloring": "none", "start": 0.5, "end": 0.5}, + line={"color": "#ff7f0e", "width": 2}, + showscale=False, + ), + ], + layout={"xaxis_title": "x", "yaxis_title": "y", "width": 640, "height": 420}, +) +write_web_figure( + fig, "docs/public/figures/heatmap.plotly.json", alt="A potential φ(x, y) with its 0.5 contour" +) +``` + +### An animation + +Frames, a slider and a Play button come along: the file is the whole animation, and the page +plays it with the plotly.js it loads. + + + +```python +frames = [go.Frame(name=f"{t:.1f}", data=[go.Scatter(y=packet(t))]) for t in times] +fig = go.Figure( + data=[go.Scatter(x=x, y=packet(0), name="u(x, t)")], + frames=frames, + layout={"sliders": [...], "updatemenus": [...], "width": 700, "height": 380}, +) +write_web_figure(fig, "docs/public/figures/packet.plotly.json", alt="A wave packet travelling to the right") +``` + +The full code of the three figures is +[`scripts/web_figure_examples.py`](https://github.com/max-models/maxplotlib/blob/devel/scripts/web_figure_examples.py), +which the docs build runs before building this site. + +### Captions and posters + +`caption` adds a caption below a figure. `poster` and `posterDark` are pictures of the figure for +light and dark mode, shown until plotly.js has drawn it and in its place without JavaScript, e.g. +for search engines and link previews. `maxplotlib.web.write_posters(fig, "docs/public/figures/waves")` +writes both (`waves.png`, `waves.dark.png`; needs kaleido). + +## Saving a canvas + +```python +import numpy as np + +from maxplotlib import Canvas + +x = np.linspace(0, 2 * np.pi, 200) +canvas = Canvas(width="10cm", ratio=0.5) +canvas.plot(x, np.sin(x), label="sin") +canvas.plot(x, np.cos(x), label="cos") +canvas.set_title("Two waves") + +canvas.savefig("docs/public/figures/waves.plotly.json", backend="plotly") +# or as a string, with a text alternative for screen readers +text = canvas.to_json(alt="A sine and a cosine wave over one period") +``` + +Any Plotly figure, from maxplotlib or not, is written the same way with +`maxplotlib.web.write_web_figure(fig, path, title=..., alt=..., aspect=..., white_plot=...)`; +`maxplotlib.web.web_figure(fig)` returns the dict instead. + +## Embedding it + +In an MDX page, with the file under `docs/public/`: + +```mdx +import PlotlyFigure from '@max-models/starlight-theme/components/PlotlyFigure.astro'; + + +``` + +The path is relative to the site root; the component adds the site's base path. + +:::note +The data palette and the hints below need starlight-theme 0.8 or later; sites on the `v0` tag +of its docs workflow get it without a change. +::: + +A figure shown in a notebook of the docs (`fig` as the last line of a cell) is embedded the same +way, without saving it: the docs build writes the notebook's Plotly outputs as figures for +`PlotlyFigure`. The [Plotly tutorial](/maxplotlib/tutorials/tutorial-08-plotly/) shows both. + +## The format + +The file is Plotly figure JSON: `data`, `layout` and, for an animation, `frames`. Compared with +the figure it was made from: + +- **no look of its own**: no template, no fixed `width` or `height` (`autosize` instead, with + `automargin` on the axes so the tick labels fit at any size), and transparent `paper_bgcolor` + and `plot_bgcolor`. The page sets text, grid and background colours from the theme. +- **the data palette**: a trace without a colour takes it from `layout.colorway`, which the page + sets to the theme's data palette. A trace whose colour comes from a default colour cycle + (Matplotlib's `C0` to `C9`, Plotly's default colorway, the Okabe–Ito palette) carries its + position in that cycle as `trace.meta.color_slot`, and the page gives it the theme's colour + of that position. A black trace, such as a reference curve, takes the text colour, so it + stays visible in dark mode. Other explicit colours and colormaps stay as they are. +- **hints for the page** in `layout.meta.web`: + +| Key | Meaning | +| --- | --- | +| `version` | the format version, `1` | +| `title` | the title, also the default caption | +| `alt` | a text alternative for the figure | +| `aspect` | width over height, so the page reserves the space before the figure loads | +| `white_plot` | keep a white plot area in dark mode, for colours chosen for a white background | + +`white_plot` is set by default for a figure converted from Matplotlib +(`layout.meta.converted_from == "matplotlib"`), whose overlays were drawn for a white axes. diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx new file mode 100644 index 0000000..6e63dbe --- /dev/null +++ b/docs/src/content/docs/index.mdx @@ -0,0 +1,50 @@ +--- +title: maxplotlib +description: A reproducible plotting module with Matplotlib, Plotly, plotext and TikZ backends. +template: splash +hero: + tagline: One canvas, four backends. Render the same figure as a PNG with Matplotlib, an interactive Plotly chart, a plotext figure in the terminal, or TikZ code for LaTeX. + actions: + - text: Get started + link: /maxplotlib/getting-started/installation/ + icon: right-arrow + - text: View on GitHub + link: https://github.com/max-models/maxplotlib + icon: external + variant: minimal +--- + +import { Card, CardGrid } from '@astrojs/starlight/components'; + +```python +import numpy as np +from maxplotlib import Canvas + +x = np.linspace(0, 2 * np.pi, 200) + +canvas, ax = Canvas.subplots() +ax.plot(x, np.sin(x)) + +canvas.show() # Matplotlib, the default +canvas.show(backend="plotly") # interactive +canvas.show(backend="plotext") # in the terminal +canvas.savefig("sine.pdf", backend="tikzfigure") # TikZ, compiled with LaTeX +``` + + + + Build a canvas, add lines and subplots, and render it with each backend: + [Quickstart](/maxplotlib/getting-started/quickstart/). + + + From plot types, styling and layers to Gantt and flame charts and xarray data: + [Tutorials](/maxplotlib/tutorials/). + + + Turn an existing Matplotlib figure into a canvas with `Canvas.from_matplotlib`: + [what is supported](/maxplotlib/guides/matplotlib-import/). + + + Every class and function, from the docstrings: [API](/maxplotlib/api/maxplotlib/). + + diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000..13ab043 --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [ + ".astro/types.d.ts", + "**/*" + ], + "exclude": [ + "dist" + ] +} diff --git a/pyproject.toml b/pyproject.toml index 5c3163a..3a06648 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,10 @@ build-backend = "setuptools.build_meta" [project] name = "maxplotlibx" -version = "0.2.0" +version = "0.3.0" description = "A reproducible plotting module with various backends and export options." readme = "README.md" -requires-python = ">=3.8" -license = {file = "LICENSE.txt"} +requires-python = ">=3.10" keywords = ["matplotlib"] authors = [{name = "Max"}] classifiers = [ @@ -31,19 +30,16 @@ xarray = [ "xarray", ] docs = [ + "kaleido", "xarray", - "myst-parser", - "sphinx", - "sphinx-rtd-theme", - "nbsphinx", + "griffe", "ipykernel", - "nbconvert", + "nbclient>=0.11", + "nbformat>=5.11.1", ] dev = [ "maxplotlibx[test,docs]", "ruff", - "black[jupyter]", - "isort", "jupyterlab", "nbstripout", "pre-commit", @@ -55,11 +51,8 @@ dev = [ [tool.setuptools.packages.find] where = ["src"] -[tool.black] -line-length = 88 - [tool.ruff] line-length = 88 - -[tool.isort] -profile = "black" +# formatting: `ruff format`; import sorting: the isort rules of `ruff check` +# an explicit rule set: ruff's default one grows between releases +lint.select = ["E4", "E7", "E9", "F", "I"] diff --git a/pytest.ini b/pytest.ini index b4fff9a..3730074 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ # pytest.ini [pytest] addopts = -ra -q -pythonpath = src \ No newline at end of file +pythonpath = src diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..3d599e5 --- /dev/null +++ b/renovate.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + "github>max-models/starlight-theme:renovate-preset" + ], + "baseBranchPatterns": [ + "devel" + ] +} diff --git a/scripts/web_figure_examples.py b/scripts/web_figure_examples.py new file mode 100644 index 0000000..62a4768 --- /dev/null +++ b/scripts/web_figure_examples.py @@ -0,0 +1,155 @@ +"""Write the example web figures of the docs' "Web figures" guide to docs/public/figures/. + +The docs build runs this before ``astro build`` (``pre-build`` in .github/workflows/docs.yml); +run it by hand for ``astro dev``: + + python scripts/web_figure_examples.py + +The code of each figure is also shown on the page, in docs/src/content/docs/guides/web-figures.mdx; +keep the two the same. +""" + +import json +from pathlib import Path + +import numpy as np +import plotly.graph_objects as go + +from maxplotlib import Canvas +from maxplotlib.web import write_posters, write_web_figure + +OUT = Path(__file__).resolve().parents[1] / "docs" / "public" / "figures" + + +def waves(): + """Lines without colours of their own, and a black reference line.""" + x = np.linspace(0, 4 * np.pi, 400) + canvas = Canvas(width="12cm", ratio=0.5) + for k in range(4): + canvas.plot( + x, np.exp(-0.1 * x) * np.sin(x - k * np.pi / 4), label=f"phase {k}/4 π" + ) + canvas.plot(x, 0 * x, color="black", linewidth=0.8) + canvas.set_xlabel("t") + canvas.set_ylabel("amplitude") + canvas.set_legend(True) + canvas.savefig(OUT / "waves.plotly.json", backend="plotly") + + +def heatmap(): + """A colormap, which stays as it is, and a contour in Matplotlib's C1.""" + x = np.linspace(-2, 2, 80) + y = np.linspace(-1.5, 1.5, 60) + X, Y = np.meshgrid(x, y) + z = np.exp(-(X**2 + Y**2)) * np.cos(3 * X) + 0.3 * np.exp( + -((X - 1) ** 2 + Y**2) * 4 + ) + fig = go.Figure( + [ + # zsmooth: a smooth field instead of visible cells (keep the cells for grid data) + go.Heatmap( + x=x, + y=y, + z=z, + colorscale="Viridis", + colorbar={"title": "φ"}, + zsmooth="best", + ), + go.Contour( + x=x, + y=y, + z=z, + contours={"coloring": "none", "start": 0.5, "end": 0.5}, + line={"color": "#ff7f0e", "width": 2}, + showscale=False, + name="φ = 0.5", + ), + ], + layout={ + "xaxis_title": "x", + "yaxis_title": "y", + "width": 640, + "height": 420, + "margin": {"t": 30}, + }, + ) + write_web_figure( + fig, OUT / "heatmap.plotly.json", alt="A potential φ(x, y) with its 0.5 contour" + ) + + +def animation(): + """An animation with a slider and Play button: a wave packet travelling to the right.""" + x = np.linspace(0, 10, 200) + times = np.linspace(0, 6, 31) + + def packet(t): + return np.exp(-((x - 2 - t) ** 2)) * np.cos(6 * (x - 2 - t)) + + frames = [ + # a frame names only what changes: the y values + go.Frame(name=f"{t:.1f}", data=[go.Scatter(y=packet(t))]) + for t in times + ] + steps = [ + { + "label": frame.name, + "method": "animate", + "args": [[frame.name], {"mode": "immediate", "frame": {"duration": 0}}], + } + for frame in frames + ] + fig = go.Figure( + data=[go.Scatter(x=x, y=packet(0), name="u(x, t)")], + frames=frames, + layout={ + "xaxis": {"title": {"text": "x", "standoff": 8}, "range": [0, 10]}, + "yaxis": {"title": "u", "range": [-1.1, 1.1]}, + "width": 700, + "height": 380, + "sliders": [ + {"steps": steps, "currentvalue": {"prefix": "t = "}, "pad": {"t": 50}} + ], + "updatemenus": [ + { + "type": "buttons", + "x": 0, + "y": 1.15, + "xanchor": "left", + "buttons": [ + { + "label": "Play", + "method": "animate", + "args": [ + None, + {"frame": {"duration": 60}, "fromcurrent": True}, + ], + } + ], + } + ], + }, + ) + write_web_figure( + fig, OUT / "packet.plotly.json", alt="A wave packet travelling to the right" + ) + + +def posters(): + """Pictures of the figures for light and dark mode, shown until plotly.js has drawn them.""" + for name in ("waves", "heatmap", "packet"): + web = json.loads((OUT / f"{name}.plotly.json").read_text()) + try: + write_posters(web, OUT / name) + except Exception as error: # no kaleido or no browser for it: no posters + print(f"skipped the posters of {name}: {error}") + return + print(f"wrote the posters of {name}") + + +if __name__ == "__main__": + OUT.mkdir(parents=True, exist_ok=True) + for example in (waves, heatmap, animation): + example() + print(f"wrote the {example.__name__} example") + posters() diff --git a/src/maxplotlib/backends/matplotlib/import_state.py b/src/maxplotlib/backends/matplotlib/import_state.py index 286dedc..fb864a8 100644 --- a/src/maxplotlib/backends/matplotlib/import_state.py +++ b/src/maxplotlib/backends/matplotlib/import_state.py @@ -175,6 +175,63 @@ def draw(self, ax, **overrides): return artist +def _text_properties(text): + return dict( + fontproperties=copy.deepcopy(text.get_fontproperties()), + color=text.get_color(), + rotation=text.get_rotation(), + rotation_mode=text.get_rotation_mode(), + horizontalalignment=text.get_ha(), + verticalalignment=text.get_va(), + visible=text.get_visible(), + usetex=text.get_usetex(), + ) + + +def _tick_params(axis, which): + tick = getattr(axis, "get_" + which + "_ticks")(1)[0] + defaults = dict( + length=tick._size, + width=tick._width, + pad=tick._base_pad, + direction=tick._tickdir, + color=tick.tick1line.get_color(), + ) + defaults.update(copy.deepcopy(getattr(axis, "_" + which + "_tick_kw"))) + return defaults + + +def _tick_line_style(line): + return { + name: getattr(line, "get_" + name)() + for name in ( + "color", + "marker", + "markersize", + "markeredgewidth", + "markeredgecolor", + "visible", + "zorder", + ) + } + + +def _tick_style(tick): + return dict( + tick1line=_tick_line_style(tick.tick1line), + tick2line=_tick_line_style(tick.tick2line), + label1=_text_properties(tick.label1), + label2=_text_properties(tick.label2), + gridline=dict( + visible=tick.gridline.get_visible(), + color=tick.gridline.get_color(), + linewidth=tick.gridline.get_linewidth(), + linestyle=tick.gridline.get_linestyle(), + alpha=tick.gridline.get_alpha(), + ), + ) + + def capture_axis_state(ax): """Capture decorations which have no backend-neutral equivalent yet.""" state = {"axes": {}, "spines": {}, "titles": {}} @@ -236,63 +293,6 @@ def capture_axis_state(ax): return state -def _text_properties(text): - return dict( - fontproperties=copy.deepcopy(text.get_fontproperties()), - color=text.get_color(), - rotation=text.get_rotation(), - rotation_mode=text.get_rotation_mode(), - horizontalalignment=text.get_ha(), - verticalalignment=text.get_va(), - visible=text.get_visible(), - usetex=text.get_usetex(), - ) - - -def _tick_params(axis, which): - tick = getattr(axis, "get_" + which + "_ticks")(1)[0] - defaults = dict( - length=tick._size, - width=tick._width, - pad=tick._base_pad, - direction=tick._tickdir, - color=tick.tick1line.get_color(), - ) - defaults.update(copy.deepcopy(getattr(axis, "_" + which + "_tick_kw"))) - return defaults - - -def _tick_line_style(line): - return { - name: getattr(line, "get_" + name)() - for name in ( - "color", - "marker", - "markersize", - "markeredgewidth", - "markeredgecolor", - "visible", - "zorder", - ) - } - - -def _tick_style(tick): - return dict( - tick1line=_tick_line_style(tick.tick1line), - tick2line=_tick_line_style(tick.tick2line), - label1=_text_properties(tick.label1), - label2=_text_properties(tick.label2), - gridline=dict( - visible=tick.gridline.get_visible(), - color=tick.gridline.get_color(), - linewidth=tick.gridline.get_linewidth(), - linestyle=tick.gridline.get_linestyle(), - alpha=tick.gridline.get_alpha(), - ), - ) - - def apply_axis_state(ax, state, *, units=True): for name, props in state["spines"].items(): props = copy.deepcopy(props) diff --git a/src/maxplotlib/backends/matplotlib/importer.py b/src/maxplotlib/backends/matplotlib/importer.py index 15036b2..80d5db6 100644 --- a/src/maxplotlib/backends/matplotlib/importer.py +++ b/src/maxplotlib/backends/matplotlib/importer.py @@ -34,275 +34,6 @@ ) -def import_matplotlib( - canvas_cls, - source, - *, - strict=False, - trusted=False, - fallback="native", - **canvas_kwargs, -): - if fallback not in ("native", "skip", "raster"): - raise ValueError("fallback must be 'native', 'skip', or 'raster'") - if isinstance(source, (str, bytes, os.PathLike)) or hasattr(source, "read"): - if not trusted: - raise ValueError( - "Serialized Matplotlib input requires trusted=True; " - "unpickling can execute arbitrary Python code" - ) - if isinstance(source, bytes): - source = pickle.loads(source) - elif hasattr(source, "read"): - source = pickle.load(source) - else: - with open(source, "rb") as stream: - source = pickle.load(stream) - report = ImportReport() - - def unsupported(message, artist=None): - artist = artist if artist is not None else unsupported.artist - report.add( - artist, - message, - severity="error" if strict else "warning", - fallback="skipped", - ) - if strict: - error = NotImplementedError(message) - error.import_report = report - raise error - warnings.warn(message, UserWarning, stacklevel=3) - - unsupported.artist = source - unsupported.report = report - unsupported.fallback = fallback - - whole_figure = isinstance(source, Figure) - explicit_shape = None - if whole_figure: - axes = list(source.axes) - elif isinstance(source, Axes): - axes = [source] - explicit_shape = (1, 1) - else: - try: - array = np.asarray(source, dtype=object) - except (TypeError, ValueError) as exc: - raise TypeError( - "Expected a Figure, Axes, or rectangular array of Axes" - ) from exc - if array.ndim not in (1, 2): - raise TypeError("Expected a Figure, Axes, or 1D/2D array of Axes") - axes = list(array.flat) - if array.ndim == 2: - explicit_shape = array.shape - if not axes: - raise ValueError("At least one Matplotlib Axes is required") - if not all(isinstance(ax, Axes) for ax in axes): - raise TypeError("Every array element must be a Matplotlib Axes") - if len({id(ax) for ax in axes}) != len(axes): - raise ValueError("Duplicate Axes are not supported") - figure = root_figure(source if whole_figure else axes[0].get_figure()) - if any(root_figure(ax.get_figure()) is not figure for ax in axes): - raise ValueError("All Axes must belong to the same Figure") - - if fallback == "raster": - return _raster_import( - canvas_cls, figure, axes, whole_figure, report, **canvas_kwargs - ) - - # Colorbar axes are decorations, not independent data subplots. - selected = [] - twins = [] - colorbars = [] - # The source figure's creation order identifies the primary axes even when - # the caller passes the twin first in an array. - for index, ax in sorted( - enumerate(axes), key=lambda item: figure.axes.index(item[1]) - ): - if getattr(ax, "_colorbar", None) is not None: - colorbars.append(ax._colorbar) - else: - parent = next( - ( - previous - for _, previous in selected - if ax in previous._twinned_axes.get_siblings(previous) - ), - None, - ) - if parent is None: - selected.append((index, ax)) - else: - direction = "x" if ax.get_shared_x_axes().joined(ax, parent) else "y" - twins.append((parent, ax, direction)) - selected.sort(key=lambda item: item[0]) - if not selected: - raise ValueError("No supported Axes to import") - - if explicit_shape is not None: - nrows, ncols = explicit_shape - positioned = [(i // ncols, i % ncols, ax) for i, ax in selected] - else: - specs = [ax.get_subplotspec() for _, ax in selected] - common_grid = specs[0].get_gridspec() if specs[0] is not None else None - simple_grid = common_grid is not None and all( - spec is not None - and spec.get_gridspec() is common_grid - and spec.rowspan.stop - spec.rowspan.start == 1 - and spec.colspan.stop - spec.colspan.start == 1 - for spec in specs - ) - if simple_grid and len({spec.num1 for spec in specs}) == len(specs): - nrows, ncols = common_grid.get_geometry() - positioned = [ - (spec.rowspan.start, spec.colspan.start, ax) - for spec, (_, ax) in zip(specs, selected) - ] - else: - # Irregular layouts use stable editable slots and retain their - # actual axes rectangles for the Matplotlib renderer. - nrows, ncols = 1, len(selected) - positioned = [(0, i, ax) for i, (_, ax) in enumerate(selected)] - - if "nrows" in canvas_kwargs or "ncols" in canvas_kwargs: - raise TypeError("nrows and ncols are inferred from the imported axes") - canvas_kwargs.setdefault("figsize", tuple(figure.get_size_inches())) - canvas_kwargs.setdefault("dpi", figure.dpi) - canvas = canvas_cls(nrows=nrows, ncols=ncols, **canvas_kwargs) - canvas.import_report = report - canvas._import_layout = {} - canvas._import_in_layout = {} - canvas._import_shared_axes = [] - canvas._import_extra_twins = [] - canvas._import_colorbars = [] - canvas._import_figure_artists = [] - source_targets = {} - source_slots = {} - for row, col, ax in positioned: - target = canvas.add_subplot(row=row, col=col) - source_targets[ax] = target - source_slots[ax] = (row, col) - if explicit_shape is None: - canvas._import_layout[(row, col)] = figure_bounds(ax, figure) - canvas._import_in_layout[(row, col)] = ax.get_in_layout() - _import_axes(ax, target, unsupported) - for parent, twin, direction in twins: - if parent is ax: - factory = canvas.twinx if direction == "x" else canvas.twiny - registry = ( - canvas._twinx_subplots - if direction == "x" - else canvas._twiny_subplots - ) - if (row, col) in registry: - from maxplotlib.subfigure.line_plot import LinePlot - - twin_target = LinePlot() - canvas._import_extra_twins.append( - ((row, col), direction, twin_target) - ) - else: - twin_target = factory(row=row, col=col) - source_targets[twin] = twin_target - _import_axes(twin, twin_target, unsupported) - if explicit_shape is None and all(ax.figure is figure for _, ax in selected): - specs = {source_slots[ax]: ax.get_subplotspec() for _, ax in selected} - canvas._import_layout_specs = ReboundSnapshot(specs, fig=figure) - canvas._import_layout_engine = copy.deepcopy(figure.get_layout_engine()) - if canvas._import_layout_engine is None: - canvas.subplots_adjust( - **{ - name: getattr(figure.subplotpars, name) - for name in ("left", "right", "bottom", "top", "wspace", "hspace") - } - ) - for name in ("x", "y"): - for index, (_, ax) in enumerate(selected): - parent = next( - ( - previous - for _, previous in selected[:index] - if getattr(ax, "get_shared_" + name + "_axes")().joined( - ax, previous - ) - ), - None, - ) - if parent is not None: - canvas._import_shared_axes.append( - (name, source_slots[parent], source_slots[ax]) - ) - for colorbar in colorbars: - owner = getattr(colorbar.mappable, "axes", None) - if owner is not None and owner not in source_targets: - unsupported("Colorbar mappable is outside the selected axes", colorbar.ax) - continue - canvas._import_colorbars.append( - _capture_colorbar(colorbar, source_targets.get(owner)) - ) - report.add(colorbar.ax, "Colorbar is linked to its imported mappable") - if whole_figure: - canvas._import_figure_patch = ReboundSnapshot(figure.patch, fig=figure) - canvas._import_figure_style = dict( - facecolor=figure.get_facecolor(), - edgecolor=figure.get_edgecolor(), - linewidth=figure.patch.get_linewidth(), - frameon=figure.get_frameon(), - ) - for getter, setter in ( - ("get_suptitle", "suptitle"), - ("get_supxlabel", "supxlabel"), - ("get_supylabel", "supylabel"), - ): - value = getattr(figure, getter, lambda: "")() - if value: - text = getattr(figure, "_" + setter) - getattr(canvas, setter)( - value, - x=text.get_position()[0], - y=text.get_position()[1], - **_text_style(text), - ) - titles = {figure._suptitle, figure._supxlabel, figure._supylabel} - for artist in ( - list(figure.legends) - + list(figure.artists) - + list(figure.lines) - + list(figure.patches) - + list(figure.images) - + [text for text in figure.texts if text not in titles] - ): - canvas._import_figure_artists.append(ReboundSnapshot(artist, fig=figure)) - report.add( - artist, "Figure decoration retained for Matplotlib", fallback="native" - ) - - def subfigure_decorations(owner): - for subfigure in owner.subfigs: - background = ReboundSnapshot(subfigure.patch, fig=subfigure) - background.payload.set_zorder( - min((ax.get_zorder() for ax in figure.axes), default=0) - 1 - ) - canvas._import_figure_artists.append(background) - for artist in ( - list(subfigure.texts) - + list(subfigure.legends) - + list(subfigure.artists) - + list(subfigure.lines) - + list(subfigure.patches) - + list(subfigure.images) - ): - canvas._import_figure_artists.append( - ReboundSnapshot(artist, fig=subfigure) - ) - subfigure_decorations(subfigure) - - subfigure_decorations(figure) - return canvas - - def _style(artist): return dict( label=artist.get_label(), @@ -409,6 +140,22 @@ def _text_style(text): return kwargs +def _needs_native_style(artist): + box = artist.get_clip_box() + custom_box = ( + box is not None + and artist.axes is not None + and not np.array_equal(box.bounds, artist.axes.bbox.bounds) + ) + return ( + custom_box + or artist.get_path_effects() + or artist.get_agg_filter() is not None + or artist.get_sketch_params() is not None + or artist.get_clip_path() is not None + ) + + def _import_errorbar(container, ax, target, unsupported): """Return whether the container was consumed as one semantic entry.""" line, caps, ranges = container.lines @@ -509,6 +256,24 @@ def _collection_style(collection, index): return kwargs +def _native(artist, ax, target, unsupported): + if unsupported.fallback == "skip" or not type(artist).__module__.startswith( + ("matplotlib.", "mpl_toolkits.") + ): + unsupported(f"Unsupported {type(artist).__name__}; artist skipped", artist) + return + snapshot = ReboundSnapshot(artist, ax) + target._add( + dict(plot_type="matplotlib_artist", snapshot=snapshot, layer=0, kwargs={}), 0 + ) + unsupported.report.add( + artist, + "Editable artist geometry retained for Matplotlib; " + "other backends require a raster import", + fallback="native", + ) + + def _import_collection(collection, ax, target, unsupported): """Import plain line/polygon collections; return False for scatter.""" if not isinstance(collection, (LineCollection, PolyCollection)): @@ -537,19 +302,77 @@ def _import_collection(collection, ax, target, unsupported): or np.any( ~np.isin(path.codes, [Path.MOVETO, Path.LINETO, Path.CLOSEPOLY]) ) - ): - _native(collection, ax, target, unsupported) - return True - vertices = path.vertices - if path.codes is not None and path.codes[-1] == Path.CLOSEPOLY: - vertices = vertices[:-1] - target.fill( - vertices[:, 0].copy(), - vertices[:, 1].copy(), - hatch=collection.get_hatch(), - **_collection_style(collection, i), + ): + _native(collection, ax, target, unsupported) + return True + vertices = path.vertices + if path.codes is not None and path.codes[-1] == Path.CLOSEPOLY: + vertices = vertices[:-1] + target.fill( + vertices[:, 0].copy(), + vertices[:, 1].copy(), + hatch=collection.get_hatch(), + **_collection_style(collection, i), + ) + return True + + +def _tracked(artists, ax, target, unsupported): + order = {id(artist): i for i, artist in enumerate(ax.get_children())} + for artist in artists: + unsupported.artist = artist + start = len(target.line_data) + yield artist + entries = target.line_data[start:] + children = getattr(artist, "get_children", lambda: [])() + index = order.get( + id(artist), + min( + (order.get(id(child), len(order)) for child in children), + default=len(order), + ), + ) + for entry in entries: + entry["source_artist_id"] = id(artist) + entry["source_container_ids"] = [ + identifier + for identifier, group in target.import_groups.items() + if identifier == id(artist) or id(artist) in group["artist_ids"] + ] + entry.setdefault("source_order", index) + if isinstance(artist, Artist): + entry["source_sticky_edges"] = ( + list(artist.sticky_edges.x), + list(artist.sticky_edges.y), + ) + entry["source_picker"] = artist.get_picker() + if entries: + unsupported.report.add( + artist, + "Imported " + str(len(entries)) + " plot entries", + fallback="geometry" if len(entries) > 1 else None, ) - return True + + +def _capture_inset_indicator(indicator): + """Style for ``indicate_inset_zoom``; connectors are recomputed on render. + + ``InsetIndicator`` spans two Axes and recomputes its rectangle/connector + geometry from live axes limits on every draw, so it cannot be captured as + detached geometry the way single-axes artists are. Recreating it with + ``Axes.indicate_inset_zoom`` on the reconstructed parent/inset pair keeps + that live behavior instead of freezing a stale snapshot. + """ + rectangle = indicator.rectangle + return dict( + facecolor=copy.deepcopy(rectangle.get_facecolor()), + edgecolor=copy.deepcopy(rectangle.get_edgecolor()), + linewidth=rectangle.get_linewidth(), + linestyle=rectangle.get_linestyle(), + alpha=indicator.get_alpha(), + zorder=indicator.get_zorder(), + visible=indicator.get_visible(), + ) def _import_axes(ax, target, unsupported): @@ -870,8 +693,7 @@ def _import_axes(ax, target, unsupported): target.set_ymargin(ax.margins()[1]) unsupported.report.add( ax, - "Axis scales, tick and spine styles, and legend layout " - "retained for Matplotlib", + "Axis scales, tick and spine styles, and legend layout retained for Matplotlib", fallback="native", ) target._import_grid = target._grid @@ -887,130 +709,337 @@ def _import_axes(ax, target, unsupported): entries.sort(key=lambda entry: entry.get("source_order", 0)) -def _needs_native_style(artist): - box = artist.get_clip_box() - custom_box = ( - box is not None - and artist.axes is not None - and not np.array_equal(box.bounds, artist.axes.bbox.bounds) +def _capture_colorbar(colorbar, target): + axis = ( + colorbar.ax.yaxis if colorbar.orientation == "vertical" else colorbar.ax.xaxis ) - return ( - custom_box - or artist.get_path_effects() - or artist.get_agg_filter() is not None - or artist.get_sketch_params() is not None - or artist.get_clip_path() is not None + return dict( + target=target, + artist_id=id(colorbar.mappable), + standalone=( + dict( + norm=copy.deepcopy(colorbar.mappable.norm), + cmap=copy.copy(colorbar.mappable.get_cmap()), + ) + if target is None + else None + ), + position=tuple(colorbar.ax.get_position().bounds), + kwargs=dict( + orientation=colorbar.orientation, + extend=colorbar.extend, + extendfrac=colorbar.extendfrac, + extendrect=colorbar.extendrect, + spacing=colorbar.spacing, + drawedges=colorbar.drawedges, + boundaries=copy.deepcopy(colorbar.boundaries), + values=copy.deepcopy(colorbar.values), + ), + label=axis.label.get_text(), + label_style=_text_style(axis.label), + ticks=copy.deepcopy(colorbar.get_ticks()), + formatter=ReboundSnapshot(colorbar.formatter, colorbar.ax), + state=capture_axis_state(colorbar.ax), ) -def _native(artist, ax, target, unsupported): - if unsupported.fallback == "skip" or not type(artist).__module__.startswith( - ("matplotlib.", "mpl_toolkits.") - ): - unsupported(f"Unsupported {type(artist).__name__}; artist skipped", artist) - return - snapshot = ReboundSnapshot(artist, ax) - target._add( - dict(plot_type="matplotlib_artist", snapshot=snapshot, layer=0, kwargs={}), 0 - ) - unsupported.report.add( - artist, - "Editable artist geometry retained for Matplotlib; " - "other backends require a raster import", - fallback="native", +def _raster_import(canvas_cls, figure, axes, whole_figure, report, **kwargs): + from matplotlib.backends.backend_agg import FigureCanvasAgg + + snapshot = copy.deepcopy(figure) + if not whole_figure: + indices = [figure.axes.index(ax) for ax in axes] + for i, ax in enumerate(list(snapshot.axes)): + if i not in indices: + ax.remove() + FigureCanvasAgg(snapshot).draw() + rgba = np.asarray(snapshot.canvas.buffer_rgba()).copy() + kwargs.setdefault("figsize", tuple(figure.get_size_inches())) + kwargs.setdefault("dpi", figure.dpi) + canvas = canvas_cls(**kwargs) + subplot = canvas.add_subplot(row=0, col=0) + subplot.add_imshow(rgba) + subplot.set_axis_off() + subplot.set_grid(False) + canvas.import_report = report + report.add( + figure, + "Raster snapshot: data, artist editing, vector paths and interactive " + "state are lost", + severity="warning", + fallback="raster", + backends=("matplotlib", "plotly"), ) + return canvas -def _tracked(artists, ax, target, unsupported): - order = {id(artist): i for i, artist in enumerate(ax.get_children())} - for artist in artists: - unsupported.artist = artist - start = len(target.line_data) - yield artist - entries = target.line_data[start:] - children = getattr(artist, "get_children", lambda: [])() - index = order.get( - id(artist), - min( - (order.get(id(child), len(order)) for child in children), - default=len(order), - ), +def import_matplotlib( + canvas_cls, + source, + *, + strict=False, + trusted=False, + fallback="native", + **canvas_kwargs, +): + if fallback not in ("native", "skip", "raster"): + raise ValueError("fallback must be 'native', 'skip', or 'raster'") + if isinstance(source, (str, bytes, os.PathLike)) or hasattr(source, "read"): + if not trusted: + raise ValueError( + "Serialized Matplotlib input requires trusted=True; " + "unpickling can execute arbitrary Python code" + ) + if isinstance(source, bytes): + source = pickle.loads(source) + elif hasattr(source, "read"): + source = pickle.load(source) + else: + with open(source, "rb") as stream: + source = pickle.load(stream) + report = ImportReport() + + def unsupported(message, artist=None): + artist = artist if artist is not None else unsupported.artist + report.add( + artist, + message, + severity="error" if strict else "warning", + fallback="skipped", + ) + if strict: + error = NotImplementedError(message) + error.import_report = report + raise error + warnings.warn(message, UserWarning, stacklevel=3) + + unsupported.artist = source + unsupported.report = report + unsupported.fallback = fallback + + whole_figure = isinstance(source, Figure) + explicit_shape = None + if whole_figure: + axes = list(source.axes) + elif isinstance(source, Axes): + axes = [source] + explicit_shape = (1, 1) + else: + try: + array = np.asarray(source, dtype=object) + except (TypeError, ValueError) as exc: + raise TypeError( + "Expected a Figure, Axes, or rectangular array of Axes" + ) from exc + if array.ndim not in (1, 2): + raise TypeError("Expected a Figure, Axes, or 1D/2D array of Axes") + axes = list(array.flat) + if array.ndim == 2: + explicit_shape = array.shape + if not axes: + raise ValueError("At least one Matplotlib Axes is required") + if not all(isinstance(ax, Axes) for ax in axes): + raise TypeError("Every array element must be a Matplotlib Axes") + if len({id(ax) for ax in axes}) != len(axes): + raise ValueError("Duplicate Axes are not supported") + figure = root_figure(source if whole_figure else axes[0].get_figure()) + if any(root_figure(ax.get_figure()) is not figure for ax in axes): + raise ValueError("All Axes must belong to the same Figure") + + if fallback == "raster": + return _raster_import( + canvas_cls, figure, axes, whole_figure, report, **canvas_kwargs + ) + + # Colorbar axes are decorations, not independent data subplots. + selected = [] + twins = [] + colorbars = [] + # The source figure's creation order identifies the primary axes even when + # the caller passes the twin first in an array. + for index, ax in sorted( + enumerate(axes), key=lambda item: figure.axes.index(item[1]) + ): + if getattr(ax, "_colorbar", None) is not None: + colorbars.append(ax._colorbar) + else: + parent = next( + ( + previous + for _, previous in selected + if ax in previous._twinned_axes.get_siblings(previous) + ), + None, + ) + if parent is None: + selected.append((index, ax)) + else: + direction = "x" if ax.get_shared_x_axes().joined(ax, parent) else "y" + twins.append((parent, ax, direction)) + selected.sort(key=lambda item: item[0]) + if not selected: + raise ValueError("No supported Axes to import") + + if explicit_shape is not None: + nrows, ncols = explicit_shape + positioned = [(i // ncols, i % ncols, ax) for i, ax in selected] + else: + specs = [ax.get_subplotspec() for _, ax in selected] + common_grid = specs[0].get_gridspec() if specs[0] is not None else None + simple_grid = common_grid is not None and all( + spec is not None + and spec.get_gridspec() is common_grid + and spec.rowspan.stop - spec.rowspan.start == 1 + and spec.colspan.stop - spec.colspan.start == 1 + for spec in specs ) - for entry in entries: - entry["source_artist_id"] = id(artist) - entry["source_container_ids"] = [ - identifier - for identifier, group in target.import_groups.items() - if identifier == id(artist) or id(artist) in group["artist_ids"] + if simple_grid and len({spec.num1 for spec in specs}) == len(specs): + nrows, ncols = common_grid.get_geometry() + positioned = [ + (spec.rowspan.start, spec.colspan.start, ax) + for spec, (_, ax) in zip(specs, selected) ] - entry.setdefault("source_order", index) - if isinstance(artist, Artist): - entry["source_sticky_edges"] = ( - list(artist.sticky_edges.x), - list(artist.sticky_edges.y), - ) - entry["source_picker"] = artist.get_picker() - if entries: - unsupported.report.add( - artist, - "Imported " + str(len(entries)) + " plot entries", - fallback="geometry" if len(entries) > 1 else None, - ) + else: + # Irregular layouts use stable editable slots and retain their + # actual axes rectangles for the Matplotlib renderer. + nrows, ncols = 1, len(selected) + positioned = [(0, i, ax) for i, (_, ax) in enumerate(selected)] + if "nrows" in canvas_kwargs or "ncols" in canvas_kwargs: + raise TypeError("nrows and ncols are inferred from the imported axes") + canvas_kwargs.setdefault("figsize", tuple(figure.get_size_inches())) + canvas_kwargs.setdefault("dpi", figure.dpi) + canvas = canvas_cls(nrows=nrows, ncols=ncols, **canvas_kwargs) + canvas.import_report = report + canvas._import_layout = {} + canvas._import_in_layout = {} + canvas._import_shared_axes = [] + canvas._import_extra_twins = [] + canvas._import_colorbars = [] + canvas._import_figure_artists = [] + source_targets = {} + source_slots = {} + for row, col, ax in positioned: + target = canvas.add_subplot(row=row, col=col) + source_targets[ax] = target + source_slots[ax] = (row, col) + if explicit_shape is None: + canvas._import_layout[(row, col)] = figure_bounds(ax, figure) + canvas._import_in_layout[(row, col)] = ax.get_in_layout() + _import_axes(ax, target, unsupported) + for parent, twin, direction in twins: + if parent is ax: + factory = canvas.twinx if direction == "x" else canvas.twiny + registry = ( + canvas._twinx_subplots + if direction == "x" + else canvas._twiny_subplots + ) + if (row, col) in registry: + from maxplotlib.subfigure.line_plot import LinePlot -def _capture_colorbar(colorbar, target): - axis = ( - colorbar.ax.yaxis if colorbar.orientation == "vertical" else colorbar.ax.xaxis - ) - return dict( - target=target, - artist_id=id(colorbar.mappable), - standalone=( - dict( - norm=copy.deepcopy(colorbar.mappable.norm), - cmap=copy.copy(colorbar.mappable.get_cmap()), + twin_target = LinePlot() + canvas._import_extra_twins.append( + ((row, col), direction, twin_target) + ) + else: + twin_target = factory(row=row, col=col) + source_targets[twin] = twin_target + _import_axes(twin, twin_target, unsupported) + if explicit_shape is None and all(ax.figure is figure for _, ax in selected): + specs = {source_slots[ax]: ax.get_subplotspec() for _, ax in selected} + canvas._import_layout_specs = ReboundSnapshot(specs, fig=figure) + canvas._import_layout_engine = copy.deepcopy(figure.get_layout_engine()) + if canvas._import_layout_engine is None: + canvas.subplots_adjust( + **{ + name: getattr(figure.subplotpars, name) + for name in ("left", "right", "bottom", "top", "wspace", "hspace") + } + ) + for name in ("x", "y"): + for index, (_, ax) in enumerate(selected): + parent = next( + ( + previous + for _, previous in selected[:index] + if getattr(ax, "get_shared_" + name + "_axes")().joined( + ax, previous + ) + ), + None, + ) + if parent is not None: + canvas._import_shared_axes.append( + (name, source_slots[parent], source_slots[ax]) + ) + for colorbar in colorbars: + owner = getattr(colorbar.mappable, "axes", None) + if owner is not None and owner not in source_targets: + unsupported("Colorbar mappable is outside the selected axes", colorbar.ax) + continue + canvas._import_colorbars.append( + _capture_colorbar(colorbar, source_targets.get(owner)) + ) + report.add(colorbar.ax, "Colorbar is linked to its imported mappable") + if whole_figure: + canvas._import_figure_patch = ReboundSnapshot(figure.patch, fig=figure) + canvas._import_figure_style = dict( + facecolor=figure.get_facecolor(), + edgecolor=figure.get_edgecolor(), + linewidth=figure.patch.get_linewidth(), + frameon=figure.get_frameon(), + ) + for getter, setter in ( + ("get_suptitle", "suptitle"), + ("get_supxlabel", "supxlabel"), + ("get_supylabel", "supylabel"), + ): + value = getattr(figure, getter, lambda: "")() + if value: + text = getattr(figure, "_" + setter) + getattr(canvas, setter)( + value, + x=text.get_position()[0], + y=text.get_position()[1], + **_text_style(text), + ) + titles = {figure._suptitle, figure._supxlabel, figure._supylabel} + for artist in ( + list(figure.legends) + + list(figure.artists) + + list(figure.lines) + + list(figure.patches) + + list(figure.images) + + [text for text in figure.texts if text not in titles] + ): + canvas._import_figure_artists.append(ReboundSnapshot(artist, fig=figure)) + report.add( + artist, "Figure decoration retained for Matplotlib", fallback="native" ) - if target is None - else None - ), - position=tuple(colorbar.ax.get_position().bounds), - kwargs=dict( - orientation=colorbar.orientation, - extend=colorbar.extend, - extendfrac=colorbar.extendfrac, - extendrect=colorbar.extendrect, - spacing=colorbar.spacing, - drawedges=colorbar.drawedges, - boundaries=copy.deepcopy(colorbar.boundaries), - values=copy.deepcopy(colorbar.values), - ), - label=axis.label.get_text(), - label_style=_text_style(axis.label), - ticks=copy.deepcopy(colorbar.get_ticks()), - formatter=ReboundSnapshot(colorbar.formatter, colorbar.ax), - state=capture_axis_state(colorbar.ax), - ) - -def _capture_inset_indicator(indicator): - """Style for ``indicate_inset_zoom``; connectors are recomputed on render. + def subfigure_decorations(owner): + for subfigure in owner.subfigs: + background = ReboundSnapshot(subfigure.patch, fig=subfigure) + background.payload.set_zorder( + min((ax.get_zorder() for ax in figure.axes), default=0) - 1 + ) + canvas._import_figure_artists.append(background) + for artist in ( + list(subfigure.texts) + + list(subfigure.legends) + + list(subfigure.artists) + + list(subfigure.lines) + + list(subfigure.patches) + + list(subfigure.images) + ): + canvas._import_figure_artists.append( + ReboundSnapshot(artist, fig=subfigure) + ) + subfigure_decorations(subfigure) - ``InsetIndicator`` spans two Axes and recomputes its rectangle/connector - geometry from live axes limits on every draw, so it cannot be captured as - detached geometry the way single-axes artists are. Recreating it with - ``Axes.indicate_inset_zoom`` on the reconstructed parent/inset pair keeps - that live behavior instead of freezing a stale snapshot. - """ - rectangle = indicator.rectangle - return dict( - facecolor=copy.deepcopy(rectangle.get_facecolor()), - edgecolor=copy.deepcopy(rectangle.get_edgecolor()), - linewidth=rectangle.get_linewidth(), - linestyle=rectangle.get_linestyle(), - alpha=indicator.get_alpha(), - zorder=indicator.get_zorder(), - visible=indicator.get_visible(), - ) + subfigure_decorations(figure) + return canvas def _import_child(child, ax, target, unsupported): @@ -1046,33 +1075,3 @@ def _import_child(child, ax, target, unsupported): unsupported.report.add( child, "Child axes retained for Matplotlib", fallback="native" ) - - -def _raster_import(canvas_cls, figure, axes, whole_figure, report, **kwargs): - from matplotlib.backends.backend_agg import FigureCanvasAgg - - snapshot = copy.deepcopy(figure) - if not whole_figure: - indices = [figure.axes.index(ax) for ax in axes] - for i, ax in enumerate(list(snapshot.axes)): - if i not in indices: - ax.remove() - FigureCanvasAgg(snapshot).draw() - rgba = np.asarray(snapshot.canvas.buffer_rgba()).copy() - kwargs.setdefault("figsize", tuple(figure.get_size_inches())) - kwargs.setdefault("dpi", figure.dpi) - canvas = canvas_cls(**kwargs) - subplot = canvas.add_subplot(row=0, col=0) - subplot.add_imshow(rgba) - subplot.set_axis_off() - subplot.set_grid(False) - canvas.import_report = report - report.add( - figure, - "Raster snapshot: data, artist editing, vector paths and interactive " - "state are lost", - severity="warning", - fallback="raster", - backends=("matplotlib", "plotly"), - ) - return canvas diff --git a/src/maxplotlib/backends/plotext/figure.py b/src/maxplotlib/backends/plotext/figure.py index 49c00c5..fdefbbe 100644 --- a/src/maxplotlib/backends/plotext/figure.py +++ b/src/maxplotlib/backends/plotext/figure.py @@ -16,9 +16,6 @@ def strip_ansi(text: str) -> str: class _Plotext6Axes: """Small compatibility surface for maxplotlib's plotext renderer.""" - def __init__(self, figure): - self._figure = figure - def _draw(self, signal, label=None): if label is not None: signal.label(label) @@ -30,6 +27,9 @@ def plot(self, x, y, **kwargs): scatter = plot + def __init__(self, figure): + self._figure = figure + def bar(self, *args, **kwargs): label = kwargs.pop("label", None) kwargs.pop("color", None) @@ -116,6 +116,15 @@ def create_plotext_figure(nrows: int = 1, ncols: int = 1): class PlotextFigure: + def savefig(self, path, append: bool = False, keep_colors: bool = False) -> None: + destination = Path(path) + mode = "a" if append else "w" + with destination.open(mode, encoding="utf-8") as handle: + handle.write(self.build(keep_colors=keep_colors)) + handle.write("\n") + + save_fig = savefig + def __init__(self, figure: Any, suptitle: str | None = None): self.figure = figure self.suptitle = suptitle @@ -131,15 +140,6 @@ def show(self) -> str: print(output) return output - def savefig(self, path, append: bool = False, keep_colors: bool = False) -> None: - destination = Path(path) - mode = "a" if append else "w" - with destination.open(mode, encoding="utf-8") as handle: - handle.write(self.build(keep_colors=keep_colors)) - handle.write("\n") - - save_fig = savefig - def __getattr__(self, name): return getattr(self.figure, name) diff --git a/src/maxplotlib/backends/tikzfigure/convert.py b/src/maxplotlib/backends/tikzfigure/convert.py index d787777..163e228 100644 --- a/src/maxplotlib/backends/tikzfigure/convert.py +++ b/src/maxplotlib/backends/tikzfigure/convert.py @@ -42,6 +42,7 @@ from matplotlib.path import Path from matplotlib.text import Annotation, Text from matplotlib.transforms import Bbox + from tikzfigure import TikzFigure from tikzfigure.core.axis import Axis2D from tikzfigure.core.plot import format_number @@ -104,63 +105,18 @@ class TikzConversionWarning(UserWarning): ) -def figure_to_tikz( - figure, - *, - raster_dpi: float = 300, - max_markers: int = 2000, - max_items: int = 500, - max_points: int = 20000, - precision: int = 6, -) -> TikzFigure: - """Convert a Matplotlib figure into a :class:`~tikzfigure.TikzFigure`. - - The figure is drawn first (without showing it), so that its layout, - autoscaled limits, ticks and legend positions are final, and is left - as it was afterwards. - - Parameters - ---------- - figure : matplotlib.figure.Figure - The figure to convert. - raster_dpi : float, optional - Resolution of the parts drawn as images (meshes, images, ...). - Default: 300. - max_markers : int, optional - Scatter plots with more points are drawn as an image. Default: 2000. - max_items : int, optional - Collections with more differently styled items (e.g. a line - collection colored by value) are drawn as an image. Default: 500. - max_points : int, optional - Lines with more points, after Matplotlib's path simplification, are - drawn as an image. Default: 20000. - precision : int, optional - Significant digits of the written coordinates. Default: 6. - - Returns - ------- - tikzfigure.TikzFigure - The figure: ``.generate_tikz()`` for the code, ``.savefig("f.pdf")`` - to compile it, ``.savefig("f.tikz")`` to write the code and its - images. - """ - converter = _FigureConverter( - figure, - raster_dpi=raster_dpi, - max_markers=max_markers, - max_items=max_items, - max_points=max_points, - precision=precision, - ) - return converter.convert() - - class _Colors: """Colors used in the figure, defined once as ``\\definecolor``.""" def __init__(self): self.names: dict[str, str] = {} + def definitions(self) -> str: + return "\n".join( + f"\\definecolor{{{name}}}{{HTML}}{{{code}}}" + for code, name in self.names.items() + ) + def __call__(self, color): """``(name, opacity)`` of a color; name ``None`` for a transparent one.""" rgba = mcolors.to_rgba(color) @@ -170,12 +126,6 @@ def __call__(self, color): name = self.names.setdefault(code, f"mpl{code}") return name, float(rgba[3]) - def definitions(self) -> str: - return "\n".join( - f"\\definecolor{{{name}}}{{HTML}}{{{code}}}" - for code, name in self.names.items() - ) - def _font(size) -> str: size = float(size) @@ -186,20 +136,73 @@ def _pt(value) -> str: return f"{float(value):.4g}pt" -def _dash(pattern) -> str | None: - """A Matplotlib ``(offset, [on, off, ...])`` dash pattern in points as TikZ.""" - if pattern is None: - return None - offset, sequence = pattern - if not sequence: - return None - parts = [] - for index, length in enumerate(sequence): - parts.append(("on" if index % 2 == 0 else "off") + " " + _pt(length)) - option = "dash pattern=" + " ".join(parts) - if offset: - option += f", dash phase={_pt(offset)}" - return option +def _all_axes(figure): + out = [] + + def walk(ax): + out.append(ax) + for child in ax.child_axes: + walk(child) + + for ax in figure.axes: + walk(ax) + return out + + +def _supported_scales(ax) -> bool: + return ax.get_xscale() in ("linear", "log") and ax.get_yscale() in ( + "linear", + "log", + ) + + +def _text_display_position(text): + return text.get_transform().transform(text.get_unitless_position()) + + +def _anchor(text) -> str: + vertical = _VA.get(text.get_va(), "") + horizontal = _HA.get(text.get_ha(), "") + if vertical == "base" and not horizontal: + return "base" + if vertical == "mid" and not horizontal: + return "mid" + anchor = " ".join(part for part in (vertical, horizontal) if part) + return anchor or "center" + + +def _text_node(text, colors, at: str) -> str: + """A ``\\node`` for a Matplotlib text, placed at the TikZ point ``at``.""" + options = [f"anchor={_anchor(text)}", "inner sep=0pt"] + rotation = text.get_rotation() + if rotation: + options.append(f"rotate={rotation:g}") + color, opacity = colors(text.get_color()) + if color is not None and color != "mpl000000": + options.append(f"text={color}") + alpha = text.get_alpha() + if alpha is not None and alpha < 1: + opacity *= alpha + if opacity < 1: + options.append(f"text opacity={opacity:.3g}") + options.append(f"font={{{_font(text.get_fontsize())}}}") + if is_multiline(text.get_text()): + alignment = getattr(text, "_multialignment", None) or text.get_ha() + options.append(f"align={alignment}") + box = text.get_bbox_patch() + if box is not None: + face, face_opacity = colors(box.get_facecolor()) + edge, _ = colors(box.get_edgecolor()) + options[1] = "inner sep=2pt" + if face is not None: + options.append(f"fill={face}") + if face_opacity < 1: + options.append(f"fill opacity={face_opacity:.3g}, text opacity=1") + if edge is not None and box.get_linewidth() > 0: + options.append(f"draw={edge}, line width={_pt(box.get_linewidth())}") + if "round" in type(box.get_boxstyle()).__name__.lower(): + options.append("rounded corners=2pt") + return f"\\node[{', '.join(options)}] at {at} {{{latex(text.get_text())}}};" class _FigureConverter: @@ -210,58 +213,6 @@ def __init__(self, figure, **options): self.tikz = TikzFigure() self.width, self.height = figure.get_size_inches() - # -- the figure ---------------------------------------------------------- - def convert(self) -> TikzFigure: - figure = self.figure - figure.draw_without_rendering() - # images are rendered from this figure; a layout engine would move the - # axes while other parts are hidden - engine = figure.get_layout_engine() - figure.set_layout_engine("none") - try: - for ax in figure.axes: - self._axes_tree(ax) - for text in figure.texts: - self._figure_text(text) - for legend in figure.legends: - if legend.get_visible(): - self._as_image(legend, "a figure legend") - finally: - figure._layout_engine = engine - self.tikz.add_package("lmodern") - self.tikz.add_package("amsmath") - definitions = self.colors.definitions() - if definitions: - self.tikz.add_raw(definitions) - return self.tikz - - def _axes_tree(self, ax): - if not ax.get_visible(): - return - if ax.name != "rectilinear" or not _supported_scales(ax): - what = ( - f"{ax.name} axes" - if ax.name != "rectilinear" - else f"{ax.get_xscale()}/{ax.get_yscale()} scaled axes" - ) - self._as_image(ax, what) - return - _AxesConverter(self, ax).convert() - for child in ax.child_axes: - self._axes_tree(child) - - def _figure_text(self, text): - if not text.get_visible() or not text.get_text().strip(): - return - if isinstance(text, Annotation): - text.update_positions(self.figure._get_renderer()) - x, y = _text_display_position(text) - if not self.figure.bbox.contains(x, y): - return - x_in, y_in = x / self.figure.dpi, y / self.figure.dpi - node = _text_node(text, self.colors, f"({x_in:.4f}in,{y_in:.4f}in)") - self.tikz.add_raw(node) - # -- images of whole parts --------------------------------------------- def _as_image(self, artist, what): """Draw ``artist`` (axes or legend) with its decorations as one image.""" @@ -292,6 +243,59 @@ def _as_image(self, artist, what): axis.add_graphics(0, 1, 0, 1, data=data, plot_options=["forget plot"]) self.tikz.axes.append(axis) + def _axes_tree(self, ax): + if not ax.get_visible(): + return + if ax.name != "rectilinear" or not _supported_scales(ax): + what = ( + f"{ax.name} axes" + if ax.name != "rectilinear" + else f"{ax.get_xscale()}/{ax.get_yscale()} scaled axes" + ) + self._as_image(ax, what) + return + _AxesConverter(self, ax).convert() + for child in ax.child_axes: + self._axes_tree(child) + + def _figure_text(self, text): + if not text.get_visible() or not text.get_text().strip(): + return + if isinstance(text, Annotation): + text.update_positions(self.figure._get_renderer()) + x, y = _text_display_position(text) + if not self.figure.bbox.contains(x, y): + return + x_in, y_in = x / self.figure.dpi, y / self.figure.dpi + node = _text_node(text, self.colors, f"({x_in:.4f}in,{y_in:.4f}in)") + self.tikz.add_raw(node) + + # -- the figure ---------------------------------------------------------- + def convert(self) -> TikzFigure: + figure = self.figure + figure.draw_without_rendering() + # images are rendered from this figure; a layout engine would move the + # axes while other parts are hidden + engine = figure.get_layout_engine() + figure.set_layout_engine("none") + try: + for ax in figure.axes: + self._axes_tree(ax) + for text in figure.texts: + self._figure_text(text) + for legend in figure.legends: + if legend.get_visible(): + self._as_image(legend, "a figure legend") + finally: + figure._layout_engine = engine + self.tikz.add_package("lmodern") + self.tikz.add_package("amsmath") + self.tikz.add_package("amssymb") + definitions = self.colors.definitions() + if definitions: + self.tikz.add_raw(definitions) + return self.tikz + def render_only(self, artists, inches: Bbox, keep_axes=None) -> bytes: """A PNG of the figure region ``inches`` showing only ``artists``. @@ -356,73 +360,150 @@ def hide(artist): return buffer.getvalue() -def _all_axes(figure): - out = [] - - def walk(ax): - out.append(ax) - for child in ax.child_axes: - walk(child) +def figure_to_tikz( + figure, + *, + raster_dpi: float = 300, + max_markers: int = 2000, + max_items: int = 500, + max_points: int = 20000, + precision: int = 6, +) -> TikzFigure: + """Convert a Matplotlib figure into a :class:`~tikzfigure.TikzFigure`. - for ax in figure.axes: - walk(ax) - return out + The figure is drawn first (without showing it), so that its layout, + autoscaled limits, ticks and legend positions are final, and is left + as it was afterwards. + Parameters + ---------- + figure : matplotlib.figure.Figure + The figure to convert. + raster_dpi : float, optional + Resolution of the parts drawn as images (meshes, images, ...). + Default: 300. + max_markers : int, optional + Scatter plots with more points are drawn as an image. Default: 2000. + max_items : int, optional + Collections with more differently styled items (e.g. a line + collection colored by value) are drawn as an image. Default: 500. + max_points : int, optional + Lines with more points, after Matplotlib's path simplification, are + drawn as an image. Default: 20000. + precision : int, optional + Significant digits of the written coordinates. Default: 6. -def _supported_scales(ax) -> bool: - return ax.get_xscale() in ("linear", "log") and ax.get_yscale() in ( - "linear", - "log", + Returns + ------- + tikzfigure.TikzFigure + The figure: ``.generate_tikz()`` for the code, ``.savefig("f.pdf")`` + to compile it, ``.savefig("f.tikz")`` to write the code and its + images. + """ + converter = _FigureConverter( + figure, + raster_dpi=raster_dpi, + max_markers=max_markers, + max_items=max_items, + max_points=max_points, + precision=precision, ) + return converter.convert() -def _text_display_position(text): - return text.get_transform().transform(text.get_unitless_position()) +#: The TikZ tips (arrows.meta, which tikzfigure loads) of Matplotlib's straight arrow styles. +_ARROW_TIPS = { + "-": ("", ""), + "->": ("", "Straight Barb"), + "<-": ("Straight Barb", ""), + "<->": ("Straight Barb", "Straight Barb"), + "-|>": ("", "Triangle"), + "<|-": ("Triangle", ""), + "<|-|>": ("Triangle", "Triangle"), +} -def _anchor(text) -> str: - vertical = _VA.get(text.get_va(), "") - horizontal = _HA.get(text.get_ha(), "") - if vertical == "base" and not horizontal: - return "base" - if vertical == "mid" and not horizontal: - return "mid" - anchor = " ".join(part for part in (vertical, horizontal) if part) - return anchor or "center" +def _hatch_pattern(hatch) -> str | None: + """The TikZ pattern (patterns library) of a Matplotlib hatch string such as ``"///"``. + Both diagonals (``"/\\"``, ``"x"``) are a cross-hatch, both straight directions + (``"+"``, ``"-|"``) a grid, one direction its lines, and ``.``, ``o``, ``O``, ``*`` + dots; ``None`` for no hatch. The density of the hatch (repeated characters) is not + kept. + """ + if not hatch: + return None + has = set(hatch) + if {"/", "\\"} <= has or has & {"x", "X"}: + return "crosshatch" + if "+" in has or {"-", "|"} <= has: + return "grid" + for character, pattern in ( + ("/", "north east lines"), + ("\\", "north west lines"), + ("-", "horizontal lines"), + ("|", "vertical lines"), + ): + if character in has: + return pattern + if has & {".", "o", "O", "*"}: + return "dots" + return None + + +def _patch_hatch(patch): + """``(hatch string, color)`` of a patch, or ``None`` without a hatch.""" + if not patch.get_hatch(): + return None + if hasattr(patch, "get_hatchcolor"): # Matplotlib >= 3.10 + color = patch.get_hatchcolor() + else: # pragma: no cover - older Matplotlib + color = getattr(patch, "_hatch_color", patch.get_edgecolor()) + return patch.get_hatch(), color -def _text_node(text, colors, at: str) -> str: - """A ``\\node`` for a Matplotlib text, placed at the TikZ point ``at``.""" - options = [f"anchor={_anchor(text)}", "inner sep=0pt"] - rotation = text.get_rotation() - if rotation: - options.append(f"rotate={rotation:g}") - color, opacity = colors(text.get_color()) - if color is not None and color != "mpl000000": - options.append(f"text={color}") - alpha = text.get_alpha() - if alpha is not None and alpha < 1: - opacity *= alpha - if opacity < 1: - options.append(f"text opacity={opacity:.3g}") - options.append(f"font={{{_font(text.get_fontsize())}}}") - if is_multiline(text.get_text()): - alignment = getattr(text, "_multialignment", None) or text.get_ha() - options.append(f"align={alignment}") - box = text.get_bbox_patch() - if box is not None: - face, face_opacity = colors(box.get_facecolor()) - edge, _ = colors(box.get_edgecolor()) - options[1] = "inner sep=2pt" - if face is not None: - options.append(f"fill={face}") - if face_opacity < 1: - options.append(f"fill opacity={face_opacity:.3g}, text opacity=1") - if edge is not None and box.get_linewidth() > 0: - options.append(f"draw={edge}, line width={_pt(box.get_linewidth())}") - if "round" in type(box.get_boxstyle()).__name__.lower(): - options.append("rounded corners=2pt") - return f"\\node[{', '.join(options)}] at {at} {{{latex(text.get_text())}}};" + +def _dash(pattern) -> str | None: + """A Matplotlib ``(offset, [on, off, ...])`` dash pattern in points as TikZ.""" + if pattern is None: + return None + offset, sequence = pattern + if not sequence: + return None + parts = [] + for index, length in enumerate(sequence): + parts.append(("on" if index % 2 == 0 else "off") + " " + _pt(length)) + option = "dash pattern=" + " ".join(parts) + if offset: + option += f", dash phase={_pt(offset)}" + return option + + +# artists drawn as images without a warning: images, meshes, filled contours, +# quivers and the like have no better form in pgfplots +_ALWAYS_RASTER = (AxesImage, Collection) + + +def _marker_name(path) -> str: + for marker in _MARKS: + style = MarkerStyle(marker) + candidate = style.get_path().transformed(style.get_transform()) + if candidate.vertices.shape == path.vertices.shape and np.allclose( + candidate.vertices, path.vertices + ): + return marker + return "o" + + +def _with_gaps(path): + """The vertices of a path as one array, its separate pieces joined by nan.""" + out = [] + for vertex, code in zip(path.vertices, path.codes): + if code == Path.STOP: + break + if code == Path.MOVETO and out: + out.append((np.nan, np.nan)) + out.append(tuple(vertex)) + return np.asarray(out, dtype=float).reshape(-1, 2) class _AxesConverter: @@ -438,60 +519,6 @@ def __init__(self, parent: _FigureConverter, ax): self.legend_entries: list = [] self.has_images = False - # -- the axis ---------------------------------------------------------- - def convert(self): - ax = self.ax - position = ax.get_position() - width, height = self.parent.width, self.parent.height - x0, y0 = position.x0 * width, position.y0 * height - self.size = (position.width * width, position.height * height) - self.inches = Bbox.from_bounds(x0, y0, *self.size) - xlim, ylim = ax.get_xlim(), ax.get_ylim() - - options = [ - "scale only axis", - "anchor=south west", - f"at={{({x0:.4f}in,{y0:.4f}in)}}", - "clip mode=individual", - "unbounded coords=jump", - "every axis plot/.append style={line join=round}", - ] - if xlim[0] > xlim[1]: - options.append("x dir=reverse") - if ylim[0] > ylim[1]: - options.append("y dir=reverse") - if not ax.axison: - options.append("hide axis") - else: - options.extend(self._frame()) - for name in ("x", "y"): - options.extend(self._ticks(name)) - options.extend(self._labels()) - options.extend(self._background()) - - self.axis = Axis2D( - xlabel=latex(ax.get_xlabel()) if ax.xaxis.get_visible() else "", - ylabel=latex(ax.get_ylabel()) if ax.yaxis.get_visible() else "", - title=latex(self._title()), - xlim=(float(min(xlim)), float(max(xlim))), - ylim=(float(min(ylim)), float(max(ylim))), - xlog=ax.get_xscale() == "log", - ylog=ax.get_yscale() == "log", - grid=None, - width=f"{self.size[0]:.4f}in", - height=f"{self.size[1]:.4f}in", - options=options, - ) - self._legend_setup() - self._contents() - self._legend_entries() - if self.has_images: - self.axis.options.append("axis on top") - grid = self._grid() - if grid: - self.axis.options.extend(grid) - self.parent.tikz.axes.append(self.axis) - def _title(self) -> str: for location in ("center", "left", "right"): title = self.ax.get_title(location) @@ -513,12 +540,20 @@ def _frame(self) -> list[str]: x = ( "box" if visible["bottom"] and visible["top"] - else "bottom" if visible["bottom"] else "top" if visible["top"] else None + else "bottom" + if visible["bottom"] + else "top" + if visible["top"] + else None ) y = ( "box" if visible["left"] and visible["right"] - else "left" if visible["left"] else "right" if visible["right"] else None + else "left" + if visible["left"] + else "right" + if visible["right"] + else None ) options.append(f"axis x line*={x}" if x else "axis x line=none") options.append(f"axis y line*={y}" if y else "axis y line=none") @@ -684,19 +719,115 @@ def _legend_label(self, artist) -> str: """Plots never make legend entries themselves; see :meth:`_legend_entries`.""" return "" - def _legend_entries(self): - """The legend as Matplotlib draws it: an image and a text per entry. + def _stroke(self, color, linewidth, dash, alpha=None) -> list[str] | None: + name, opacity = self.colors(color) + if name is None or linewidth <= 0: + return None + if alpha is not None: + opacity *= alpha + options = [f"draw={name}", f"line width={_pt(linewidth)}"] + if opacity < 1: + options.append(f"draw opacity={opacity:.3g}") + options.append(dash or "solid") + return options - The plots are left out of the legend (``forget plot``), so that the - entries keep the legend's order and style, also for artists drawn as - images. - """ - for handle, label in self.legend_entries: - options = self._legend_image(handle) - self.axis.add_raw( - f"\\addlegendimage{{{', '.join(options)}}}\n" - f"\\addlegendentry{{{latex(label)}}}" + def _fill(self, color, alpha=None) -> list[str] | None: + name, opacity = self.colors(color) + if name is None: + return None + if alpha is not None: + opacity *= alpha + options = [f"fill={name}"] + if opacity < 1: + options.append(f"fill opacity={opacity:.3g}") + return options + + def _mark(self, marker, size, face, edge, edge_width, alpha=None) -> list[str]: + """``mark=...`` options for a Matplotlib marker of ``size`` points.""" + if isinstance(marker, str) and marker in _MARKS: + filled_mark, hollow_mark, rotation, factor = _MARKS[marker] + else: + filled_mark, hollow_mark, rotation, factor = _MARKS["o"] + fill = self._fill(face, alpha) + mark = filled_mark if fill else hollow_mark + mark_options = ["solid"] + if rotation: + mark_options.append(f"rotate={rotation}") + mark_options.extend(fill or ["fill=none"]) + stroke = self._stroke(edge, edge_width, None, alpha) + if stroke: + mark_options.extend(option for option in stroke if option != "solid") + else: + mark_options.append("draw=none") + return [ + f"mark={mark}", + f"mark size={_pt(max(size * factor, 0.1))}", + f"mark options={{{', '.join(mark_options)}}}", + ] + + def _line_options(self, line) -> list[str] | None: + """The style of a Line2D as plot options; None if it draws nothing.""" + marker = line.get_marker() + has_marker = marker not in _NO_MARKER and line.get_markersize() > 0 + has_line = line.get_linestyle() not in _NO_LINE and line.get_linewidth() > 0 + alpha = line.get_alpha() + options = [] + if has_line: + dash = _dash(line._dash_pattern) if line.is_dashed() else None + stroke = self._stroke(line.get_color(), line.get_linewidth(), dash, alpha) + if stroke is None: + has_line = False + else: + options.extend(stroke) + if not has_line: + if not has_marker: + return None + options.append("only marks") + if has_marker: + face = line.get_markerfacecolor() + if line.get_fillstyle() == "none": + face = "none" + options.extend( + self._mark( + marker, + line.get_markersize(), + face, + line.get_markeredgecolor(), + line.get_markeredgewidth(), + alpha, + ) ) + else: + options.append("mark=none") + return options + + def _hatch(self, hatch) -> list[str] | None: + """The pattern options of a hatch ``(hatch string, color)``, drawn over the fill. + + The pattern is a ``postaction``, so it covers the fill instead of replacing it, and + loads TikZ's patterns library (a ``.tikz`` file needs ``\\usetikzlibrary{patterns}``). + """ + if hatch is None: + return None + hatch, color = hatch + pattern = _hatch_pattern(hatch) + name, _ = self.colors(color) + if pattern is None or name is None: + return None + self.parent.tikz.usetikzlibrary("patterns") + return [f"postaction={{pattern={pattern}, pattern color={name}}}"] + + def _area_options(self, face, edge, width, dash, alpha=None, hatch=None): + fill = self._fill(face, alpha) + stroke = self._stroke(edge, width, dash, alpha) + pattern = self._hatch(hatch) + if fill is None and stroke is None and pattern is None: + return None + options = list(fill or ["fill=none"]) + options.extend(stroke or ["draw=none"]) + options.extend(pattern or []) + options.extend(["mark=none", "area legend"]) + return options def _legend_image(self, handle) -> list[str]: if isinstance(handle, Line2D): @@ -711,6 +842,7 @@ def _legend_image(self, handle) -> list[str]: handle.get_edgecolor(), handle.get_linewidth(), _dash(handle._dash_pattern), + hatch=_patch_hatch(handle), ) if options: return options @@ -740,31 +872,19 @@ def _legend_image(self, handle) -> list[str]: return stroke + ["mark=none"] return ["empty legend"] - # -- what is drawn ----------------------------------------------------- - def _contents(self): - ax = self.ax - children = list(getattr(ax, "_children", [])) - if not children: # an older Matplotlib - children = ( - ax.collections - + ax.patches - + ax.lines - + ax.texts - + ax.images - + ax.tables + def _legend_entries(self): + """The legend as Matplotlib draws it: an image and a text per entry. + + The plots are left out of the legend (``forget plot``), so that the + entries keep the legend's order and style, also for artists drawn as + images. + """ + for handle, label in self.legend_entries: + options = self._legend_image(handle) + self.axis.add_raw( + f"\\addlegendimage{{{', '.join(options)}}}\n" + f"\\addlegendentry{{{latex(label)}}}" ) - children = [child for child in children if child.get_visible()] - children.sort(key=lambda artist: artist.get_zorder()) # stable: drawing order - pending_images = [] - for artist in children: - emit = None if self.colorbar is not None else self._vector(artist) - if emit is None: - pending_images.append(artist) - continue - self._flush_images(pending_images) - pending_images = [] - emit() - self._flush_images(pending_images) def _flush_images(self, artists): if not artists: @@ -794,26 +914,6 @@ def _flush_images(self, artists): ) self.has_images = True - def _vector(self, artist): - """A function adding ``artist`` to the axis as vector graphics, or None.""" - if isinstance(artist, Line2D): - return self._line(artist) - if isinstance(artist, ContourSet): - return self._contour_lines(artist) - if isinstance(artist, PathCollection): - return self._scatter(artist) - if isinstance(artist, LineCollection): - return self._line_collection(artist) - if type(artist) is PolyCollection or type(artist).__name__ in ( - "FillBetweenPolyCollection", - ): - return self._polygons(artist) - if isinstance(artist, Patch): - return self._patch(artist) - if isinstance(artist, Text): - return self._text(artist) - return None - # coordinates def _view(self): """The axes box in display coordinates, grown by one box size on each side. @@ -880,20 +980,6 @@ def _to_data(self, display): data[np.abs(data[:, index]) < tiny, index] = 0.0 return data - def _add(self, data, options, label="", cycle=False, clip=True): - data = np.asarray(data, dtype=float) - if not clip: - self.axis.add_raw(self._draw(data, options, cycle)) - return - self.axis.add_plot( - x=data[:, 0].tolist(), - y=data[:, 1].tolist(), - label=label, - options=list(options) + ["forget plot"], - cycle=cycle, - precision=self.options["precision"], - ) - def _draw(self, data, options, cycle) -> str: """A ``\\draw`` path in axis coordinates, which pgfplots does not clip.""" precision = self.options["precision"] @@ -917,51 +1003,47 @@ def _draw(self, data, options, cycle) -> str: parts.append("-- cycle") return f"\\draw[{', '.join(keep)}] {' '.join(parts)};" - def _stroke(self, color, linewidth, dash, alpha=None) -> list[str] | None: - name, opacity = self.colors(color) - if name is None or linewidth <= 0: - return None - if alpha is not None: - opacity *= alpha - options = [f"draw={name}", f"line width={_pt(linewidth)}"] - if opacity < 1: - options.append(f"draw opacity={opacity:.3g}") - options.append(dash or "solid") - return options - - def _fill(self, color, alpha=None) -> list[str] | None: - name, opacity = self.colors(color) - if name is None: - return None - if alpha is not None: - opacity *= alpha - options = [f"fill={name}"] - if opacity < 1: - options.append(f"fill opacity={opacity:.3g}") - return options + def _draw_loops(self, loops, options) -> str: + """One ``\\draw`` path of several closed loops, filled by the even-odd rule. - def _mark(self, marker, size, face, edge, edge_width, alpha=None) -> list[str]: - """``mark=...`` options for a Matplotlib marker of ``size`` points.""" - if isinstance(marker, str) and marker in _MARKS: - filled_mark, hollow_mark, rotation, factor = _MARKS[marker] - else: - filled_mark, hollow_mark, rotation, factor = _MARKS["o"] - fill = self._fill(face, alpha) - mark = filled_mark if fill else hollow_mark - mark_options = ["solid"] - if rotation: - mark_options.append(f"rotate={rotation}") - mark_options.extend(fill or ["fill=none"]) - stroke = self._stroke(edge, edge_width, None, alpha) - if stroke: - mark_options.extend(option for option in stroke if option != "solid") - else: - mark_options.append("draw=none") - return [ - f"mark={mark}", - f"mark size={_pt(max(size * factor, 0.1))}", - f"mark options={{{', '.join(mark_options)}}}", + A compound patch (a shape with holes, such as an annulus or a plate with an aperture) + is one path in Matplotlib; drawn as one path in TikZ its holes stay empty, where one + filled plot per loop would fill them. + """ + precision = self.options["precision"] + keep = [ + option + for option in options + if not option.startswith( + ("mark", "only marks", "forget plot", "area legend") + ) ] + keep.append("even odd rule") + parts = [] + for loop in loops: + points = [ + f"(axis cs:{format_number(float(x), precision)}," + f"{format_number(float(y), precision)})" + for x, y in loop + if np.isfinite(x) and np.isfinite(y) + ] + if len(points) > 2: + parts.append(" -- ".join(points) + " -- cycle") + return f"\\draw[{', '.join(keep)}] {' '.join(parts)};" + + def _add(self, data, options, label="", cycle=False, clip=True): + data = np.asarray(data, dtype=float) + if not clip: + self.axis.add_raw(self._draw(data, options, cycle)) + return + self.axis.add_plot( + x=data[:, 0].tolist(), + y=data[:, 1].tolist(), + label=label, + options=list(options) + ["forget plot"], + cycle=cycle, + precision=self.options["precision"], + ) # artists def _line(self, line): @@ -985,41 +1067,139 @@ def _line(self, line): clip = line.get_clip_on() return lambda: self._add(data, options, label, clip=clip) - def _line_options(self, line) -> list[str] | None: - """The style of a Line2D as plot options; None if it draws nothing.""" - marker = line.get_marker() - has_marker = marker not in _NO_MARKER and line.get_markersize() > 0 - has_line = line.get_linestyle() not in _NO_LINE and line.get_linewidth() > 0 - alpha = line.get_alpha() - options = [] - if has_line: - dash = _dash(line._dash_pattern) if line.is_dashed() else None - stroke = self._stroke(line.get_color(), line.get_linewidth(), dash, alpha) - if stroke is None: - has_line = False - else: - options.extend(stroke) - if not has_line: - if not has_marker: + def _arrow(self, patch): + """A straight ``FancyArrowPatch`` as a ``\\draw`` with TikZ arrow tips, or ``None``. + + The tips of ``->``, ``<-``, ``<->`` and their filled forms (``-|>``, ``<|-``, + ``<|-|>``) become ``Straight Barb`` and ``Triangle`` tips of the size Matplotlib + gives them (the arrow style's head sizes times ``mutation_scale``); ``shrinkA`` and + ``shrinkB`` shorten the line. Other arrow styles and curved connections are drawn as + the paths Matplotlib renders. + """ + style = patch.get_arrowstyle() + arrow = getattr(style, "arrow", None) + ends = getattr(patch, "_posA_posB", None) + if arrow not in _ARROW_TIPS or ends is None or ends[0] is None: + return None + connection = type(patch.get_connectionstyle()).__name__ + if connection != "Arc3" or getattr(patch.get_connectionstyle(), "rad", 0.0): + return None # a curved connection + options = self._stroke( + patch.get_edgecolor(), patch.get_linewidth(), _dash(patch._dash_pattern) + ) + if options is None: + return lambda: None + scale = float(patch.get_mutation_scale()) + length = _pt(getattr(style, "head_length", 0.4) * scale) + width = _pt(2 * getattr(style, "head_width", 0.2) * scale) + begin, end = _ARROW_TIPS[arrow] + tips = "-".join( + f"{{{tip}[length={length}, width={width}]}}" if tip else "" + for tip in (begin, end) + ) + options = [tips, *options] + for key, shrink in (("<", patch.shrinkA), (">", patch.shrinkB)): + if shrink: + options.append(f"shorten {key}={_pt(shrink)}") + display = patch.get_transform().transform(np.asarray(ends, dtype=float)) + data = self._to_data(display) + + def emit(): + self.axis.add_raw(self._draw(data, options, False)) + + return emit + + def _patch(self, patch, clip=None): + if clip is None: + clip = patch.get_clip_on() + if isinstance(patch, FancyArrowPatch): + emit = self._arrow(patch) + if emit is not None: + return emit + transform = patch.get_transform() + path = patch.get_path() + fillable = None + if isinstance(patch, FancyArrowPatch): + try: + paths, fillable = patch._get_path_in_displaycoord() + except Exception: # pragma: no cover - private Matplotlib API + return None + if not np.iterable(fillable): + paths, fillable = [paths], [fillable] + transform = None + else: + paths, fillable = [path], [True] + # the patch's colors include its alpha + face = patch.get_facecolor() if patch.get_fill() else "none" + dash = _dash(patch._dash_pattern) + hatch = _patch_hatch(patch) + pieces = [] + for sub_path, can_fill in zip(paths, fillable): + if transform is not None: + sub_path = transform.transform_path(sub_path) + polygons = sub_path.to_polygons(closed_only=False) + options = self._area_options( + face if can_fill else "none", + patch.get_edgecolor(), + patch.get_linewidth(), + dash, + hatch=hatch if can_fill else None, + ) + if options is None: + continue + closed_polygons = [polygon for polygon in polygons if len(polygon) > 2] + if can_fill and len(polygons) > 1 and len(closed_polygons) == len(polygons): + # one filled path of several loops: holes stay holes (even-odd rule) + loops = [self._clip_polygon(polygon) for polygon in polygons] + loops = [self._to_data(loop) for loop in loops if len(loop)] + if loops: + pieces.append((loops, options, "compound")) + continue + for polygon in polygons: + closed = can_fill and len(polygon) > 2 + polygon = ( + self._clip_polygon(polygon) if closed else self._clip_line(polygon) + ) + if len(polygon): + pieces.append((self._to_data(polygon), options, closed)) + label = self._legend_label(patch) + + def emit(): + for index, (data, options, closed) in enumerate(pieces): + if closed == "compound": + self.axis.add_raw(self._draw_loops(data, options)) + continue + self._add( + data, options, label if index == 0 else "", cycle=closed, clip=clip + ) + + return emit + + def _text(self, text): + if isinstance(text, Annotation): + text.update_positions(self.figure._get_renderer()) + arrow = ( + getattr(text, "arrow_patch", None) if isinstance(text, Annotation) else None + ) + arrow_emit = None + if arrow is not None and arrow.get_visible(): + arrow_emit = self._patch(arrow, clip=False) + if arrow_emit is None: return None - options.append("only marks") - if has_marker: - face = line.get_markerfacecolor() - if line.get_fillstyle() == "none": - face = "none" - options.extend( - self._mark( - marker, - line.get_markersize(), - face, - line.get_markeredgecolor(), - line.get_markeredgewidth(), - alpha, - ) - ) - else: - options.append("mark=none") - return options + if not text.get_text().strip(): # an arrow without text, or nothing at all + return arrow_emit or (lambda: None) + display = _text_display_position(text) + if not self.figure.bbox.contains(*display): + return arrow_emit or (lambda: None) + fx, fy = self.ax.transAxes.inverted().transform(display) + node = _text_node(text, self.colors, f"(axis description cs:{fx:.5g},{fy:.5g})") + + def emit(): + if arrow_emit is not None: + arrow_emit() + self.axis.add_raw(node) + + return emit def _contour_lines(self, contours): if contours.filled: @@ -1217,115 +1397,102 @@ def emit(): return emit - def _area_options(self, face, edge, width, dash, alpha=None): - fill = self._fill(face, alpha) - stroke = self._stroke(edge, width, dash, alpha) - if fill is None and stroke is None: - return None - options = list(fill or ["fill=none"]) - options.extend(stroke or ["draw=none"]) - options.extend(["mark=none", "area legend"]) - return options + def _vector(self, artist): + """A function adding ``artist`` to the axis as vector graphics, or None.""" + if isinstance(artist, Line2D): + return self._line(artist) + if isinstance(artist, ContourSet): + return self._contour_lines(artist) + if isinstance(artist, PathCollection): + return self._scatter(artist) + if isinstance(artist, LineCollection): + return self._line_collection(artist) + if type(artist) is PolyCollection or type(artist).__name__ in ( + "FillBetweenPolyCollection", + ): + return self._polygons(artist) + if isinstance(artist, Patch): + return self._patch(artist) + if isinstance(artist, Text): + return self._text(artist) + return None - def _patch(self, patch, clip=None): - if clip is None: - clip = patch.get_clip_on() - transform = patch.get_transform() - path = patch.get_path() - fillable = None - if isinstance(patch, FancyArrowPatch): - try: - paths, fillable = patch._get_path_in_displaycoord() - except Exception: # pragma: no cover - private Matplotlib API - return None - if not np.iterable(fillable): - paths, fillable = [paths], [fillable] - transform = None - else: - paths, fillable = [path], [True] - # the patch's colors include its alpha - face = patch.get_facecolor() if patch.get_fill() else "none" - dash = _dash(patch._dash_pattern) - pieces = [] - for sub_path, can_fill in zip(paths, fillable): - if transform is not None: - sub_path = transform.transform_path(sub_path) - polygons = sub_path.to_polygons(closed_only=False) - options = self._area_options( - face if can_fill else "none", - patch.get_edgecolor(), - patch.get_linewidth(), - dash, + # -- what is drawn ----------------------------------------------------- + def _contents(self): + ax = self.ax + children = list(getattr(ax, "_children", [])) + if not children: # an older Matplotlib + children = ( + ax.collections + + ax.patches + + ax.lines + + ax.texts + + ax.images + + ax.tables ) - if options is None: + children = [child for child in children if child.get_visible()] + children.sort(key=lambda artist: artist.get_zorder()) # stable: drawing order + pending_images = [] + for artist in children: + emit = None if self.colorbar is not None else self._vector(artist) + if emit is None: + pending_images.append(artist) continue - for polygon in polygons: - closed = can_fill and len(polygon) > 2 - polygon = ( - self._clip_polygon(polygon) if closed else self._clip_line(polygon) - ) - if len(polygon): - pieces.append((self._to_data(polygon), options, closed)) - label = self._legend_label(patch) + self._flush_images(pending_images) + pending_images = [] + emit() + self._flush_images(pending_images) - def emit(): - for index, (data, options, closed) in enumerate(pieces): - self._add( - data, options, label if index == 0 else "", cycle=closed, clip=clip - ) + # -- the axis ---------------------------------------------------------- + def convert(self): + ax = self.ax + position = ax.get_position() + width, height = self.parent.width, self.parent.height + x0, y0 = position.x0 * width, position.y0 * height + self.size = (position.width * width, position.height * height) + self.inches = Bbox.from_bounds(x0, y0, *self.size) + xlim, ylim = ax.get_xlim(), ax.get_ylim() - return emit + options = [ + "scale only axis", + "anchor=south west", + f"at={{({x0:.4f}in,{y0:.4f}in)}}", + "clip mode=individual", + "unbounded coords=jump", + "every axis plot/.append style={line join=round}", + ] + if xlim[0] > xlim[1]: + options.append("x dir=reverse") + if ylim[0] > ylim[1]: + options.append("y dir=reverse") + if not ax.axison: + options.append("hide axis") + else: + options.extend(self._frame()) + for name in ("x", "y"): + options.extend(self._ticks(name)) + options.extend(self._labels()) + options.extend(self._background()) - def _text(self, text): - if not text.get_text().strip(): - return lambda: None - if isinstance(text, Annotation): - text.update_positions(self.figure._get_renderer()) - arrow = ( - getattr(text, "arrow_patch", None) if isinstance(text, Annotation) else None + self.axis = Axis2D( + xlabel=latex(ax.get_xlabel()) if ax.xaxis.get_visible() else "", + ylabel=latex(ax.get_ylabel()) if ax.yaxis.get_visible() else "", + title=latex(self._title()), + xlim=(float(min(xlim)), float(max(xlim))), + ylim=(float(min(ylim)), float(max(ylim))), + xlog=ax.get_xscale() == "log", + ylog=ax.get_yscale() == "log", + grid=None, + width=f"{self.size[0]:.4f}in", + height=f"{self.size[1]:.4f}in", + options=options, ) - arrow_emit = None - if arrow is not None and arrow.get_visible(): - arrow_emit = self._patch(arrow, clip=False) - if arrow_emit is None: - return None - display = _text_display_position(text) - if not self.figure.bbox.contains(*display): - return arrow_emit or (lambda: None) - fx, fy = self.ax.transAxes.inverted().transform(display) - node = _text_node(text, self.colors, f"(axis description cs:{fx:.5g},{fy:.5g})") - - def emit(): - if arrow_emit is not None: - arrow_emit() - self.axis.add_raw(node) - - return emit - - -# artists drawn as images without a warning: images, meshes, filled contours, -# quivers and the like have no better form in pgfplots -_ALWAYS_RASTER = (AxesImage, Collection) - - -def _marker_name(path) -> str: - for marker in _MARKS: - style = MarkerStyle(marker) - candidate = style.get_path().transformed(style.get_transform()) - if candidate.vertices.shape == path.vertices.shape and np.allclose( - candidate.vertices, path.vertices - ): - return marker - return "o" - - -def _with_gaps(path): - """The vertices of a path as one array, its separate pieces joined by nan.""" - out = [] - for vertex, code in zip(path.vertices, path.codes): - if code == Path.STOP: - break - if code == Path.MOVETO and out: - out.append((np.nan, np.nan)) - out.append(tuple(vertex)) - return np.asarray(out, dtype=float).reshape(-1, 2) + self._legend_setup() + self._contents() + self._legend_entries() + if self.has_images: + self.axis.options.append("axis on top") + grid = self._grid() + if grid: + self.axis.options.extend(grid) + self.parent.tikz.axes.append(self.axis) diff --git a/src/maxplotlib/backends/tikzfigure/text.py b/src/maxplotlib/backends/tikzfigure/text.py index 616fb9c..2f5cd2f 100644 --- a/src/maxplotlib/backends/tikzfigure/text.py +++ b/src/maxplotlib/backends/tikzfigure/text.py @@ -11,6 +11,7 @@ # Unicode characters pdflatex does not typeset by default, as math. _UNICODE_MATH = { + "⌀": r"\varnothing", "α": r"\alpha", "β": r"\beta", "γ": r"\gamma", diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index fb08d64..2afde69 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -41,6 +41,17 @@ def _parse_bool_env_var(name: str, default: bool = False) -> bool: return value.strip().lower() in {"1", "true", "yes", "on"} +def _running_in_jupyter() -> bool: + """Return whether the current process is running in a Jupyter kernel.""" + try: + from IPython import get_ipython + except ImportError: + return False + + shell = get_ipython() + return shell is not None and "IPKernelApp" in getattr(shell, "config", {}) + + def _display_matplotlib_figure_in_notebook(fig) -> bool: """Display a Matplotlib figure through IPython when running in Jupyter.""" try: @@ -55,17 +66,6 @@ def _display_matplotlib_figure_in_notebook(fig) -> bool: return True -def _running_in_jupyter() -> bool: - """Return whether the current process is running in a Jupyter kernel.""" - try: - from IPython import get_ipython - except ImportError: - return False - - shell = get_ipython() - return shell is not None and "IPKernelApp" in getattr(shell, "config", {}) - - _MATPLOTLIB_LOC_TO_PLOTLY_ANCHOR = { "upper right": dict(x=1.0, y=1.0, xanchor="right", yanchor="top"), "upper left": dict(x=0.0, y=1.0, xanchor="left", yanchor="top"), @@ -260,7 +260,6 @@ def plot_matplotlib(tikzfigure: TikzFigure, ax, layers=None): node.x, node.y, node.content, - fontsize=self._fontsize, ha="center", va="center", wrap=True, @@ -291,6 +290,15 @@ class Canvas: by drawing order). See :class:`~maxplotlib.subfigure.line_plot.LinePlot`. """ + _FACET_KINDS = { + "plot": "plot", + "scatter": "scatter", + "pcolormesh": "pcolormesh", + "imshow": "add_imshow", + "contour": "contour", + "contourf": "contourf", + } + def __init__( self, nrows: int = 1, @@ -485,15 +493,6 @@ def subplots( return canvas, [row[0] for row in axes] return canvas, axes - _FACET_KINDS = { - "plot": "plot", - "scatter": "scatter", - "pcolormesh": "pcolormesh", - "imshow": "add_imshow", - "contour": "contour", - "contourf": "contourf", - } - @classmethod def facet( cls, @@ -1400,9 +1399,6 @@ def set_legend( def legend(self, row=None, col=None, **kwargs): self._get_or_create_subplot(row, col).set_legend(**kwargs) - def legend(self, row=None, col=None, **kwargs): - self._get_or_create_subplot(row, col).set_legend(**kwargs) - def tick_params(self, row: int | None = None, col: int | None = None, **kwargs): """Configure major/minor tick appearance for a subplot.""" self._get_or_create_subplot(row, col).tick_params(**kwargs) @@ -2064,6 +2060,106 @@ def add_subplot( self._subplots[label] = line_plot return line_plot + def _render( + self, + backend: Backends = "matplotlib", + savefig: bool = False, + layers: list | None = None, + usetex: bool | None = None, + verbose: bool = False, + matplotlib_postprocess=None, + matplotlib_customizations=None, + allow_unsupported: bool = False, + ): + """Render the canvas. + + ``matplotlib_customizations`` accepts either a callable receiving + ``(figure, axes)`` or a mapping whose ``figure`` methods run once and + whose ``axes`` methods run on every axes. Values are keyword arguments, + or use ``{"args": [...], "kwargs": {...}}`` for positional and keyword + arguments. A scalar value is passed as one positional argument. + + ``matplotlib_postprocess`` is an optional callable receiving + ``(figure, axes)`` after a Matplotlib figure has been created. It can + call any Matplotlib API, including APIs not wrapped by maxplotlib. + Both options are only valid with the Matplotlib backend. + """ + resolved_usetex = self._usetex if usetex is None else usetex + + if verbose: + print(f"Plotting figure using backend: {backend}") + + if backend == "matplotlib": + return self.plot_matplotlib( + savefig=savefig, + layers=layers, + usetex=resolved_usetex, + verbose=verbose, + matplotlib_postprocess=matplotlib_postprocess, + matplotlib_customizations=matplotlib_customizations, + ) + elif ( + matplotlib_postprocess is not None or matplotlib_customizations is not None + ): + raise ValueError( + "Matplotlib customizations are only supported with the matplotlib backend" + ) + elif backend == "plotly": + return self.plot_plotly( + savefig=savefig, + layers=layers, + usetex=resolved_usetex, + verbose=verbose, + allow_unsupported=allow_unsupported, + ) + elif backend == "plotext": + return self.plot_plotext( + savefig=savefig, + layers=layers, + verbose=verbose, + ) + elif backend == "tikzfigure": + return self.plot_tikzfigure(savefig=savefig, layers=layers, verbose=verbose) + else: + raise ValueError(f"Invalid backend: {backend}") + + def _web_aspect(self) -> float | None: + """Width over height of the canvas, when its size says so.""" + if self._figsize is not None: + width, height = self._figsize + return float(width) / float(height) + if self._ratio == "golden": + return 2 / (5**0.5 - 1) + if isinstance(self._ratio, (int, float)) and self._ratio > 0: + return 1 / float(self._ratio) + return None + + def _save_plotly( + self, fig, filename: str, include_plotlyjs: bool | str = True + ) -> None: + _, extension = os.path.splitext(filename) + extension = extension.lower() + if extension in {".html", ".htm"}: + fig.write_html(filename, include_plotlyjs=include_plotlyjs) + return + if extension == ".json": + # a web figure for the docs sites, see maxplotlib.web + from maxplotlib.web import write_web_figure + + write_web_figure(fig, filename, aspect=self._web_aspect()) + return + try: + fig.write_image(filename) + except Exception as exc: + raise RuntimeError( + "Plotly image export failed. For PNG/PDF/SVG export, install kaleido " + "(e.g., `pip install -U kaleido`), or export to HTML instead." + ) from exc + + def _matplotlib_style(self): + """The canvas style of the last Matplotlib drawing, as an ``rc_context``.""" + return plt.rc_context(getattr(self, "_matplotlib_rc", None) or {}) + def savefig( self, filename, @@ -2096,7 +2192,8 @@ def savefig( ) _fn = f"{filename_no_extension}_{layers}.{extension}" savefig_kwargs = {"dpi": self.dpi} if self.dpi is not None else {} - fig.savefig(_fn, **savefig_kwargs) + with self._matplotlib_style(): + fig.savefig(_fn, **savefig_kwargs) print(f"Saved {_fn}") else: if layers is None: @@ -2107,7 +2204,8 @@ def savefig( if self._plotted: savefig_kwargs = {"dpi": self.dpi} if self.dpi is not None else {} - self._matplotlib_fig.savefig(full_filepath, **savefig_kwargs) + with self._matplotlib_style(): + self._matplotlib_fig.savefig(full_filepath, **savefig_kwargs) else: fig, axs = self._render( backend="matplotlib", @@ -2115,7 +2213,8 @@ def savefig( layers=layers, ) savefig_kwargs = {"dpi": self.dpi} if self.dpi is not None else {} - fig.savefig(full_filepath, **savefig_kwargs) + with self._matplotlib_style(): + fig.savefig(full_filepath, **savefig_kwargs) if verbose: print(f"Saved {full_filepath}") elif backend == "plotext": @@ -2185,69 +2284,6 @@ def savefig( if verbose: print(f"Saved {filename}") - def _render( - self, - backend: Backends = "matplotlib", - savefig: bool = False, - layers: list | None = None, - usetex: bool | None = None, - verbose: bool = False, - matplotlib_postprocess=None, - matplotlib_customizations=None, - allow_unsupported: bool = False, - ): - """Render the canvas. - - ``matplotlib_customizations`` accepts either a callable receiving - ``(figure, axes)`` or a mapping whose ``figure`` methods run once and - whose ``axes`` methods run on every axes. Values are keyword arguments, - or use ``{"args": [...], "kwargs": {...}}`` for positional and keyword - arguments. A scalar value is passed as one positional argument. - - ``matplotlib_postprocess`` is an optional callable receiving - ``(figure, axes)`` after a Matplotlib figure has been created. It can - call any Matplotlib API, including APIs not wrapped by maxplotlib. - Both options are only valid with the Matplotlib backend. - """ - resolved_usetex = self._usetex if usetex is None else usetex - - if verbose: - print(f"Plotting figure using backend: {backend}") - - if backend == "matplotlib": - return self.plot_matplotlib( - savefig=savefig, - layers=layers, - usetex=resolved_usetex, - verbose=verbose, - matplotlib_postprocess=matplotlib_postprocess, - matplotlib_customizations=matplotlib_customizations, - ) - elif ( - matplotlib_postprocess is not None or matplotlib_customizations is not None - ): - raise ValueError( - "Matplotlib customizations are only supported with the matplotlib backend" - ) - elif backend == "plotly": - return self.plot_plotly( - savefig=savefig, - layers=layers, - usetex=resolved_usetex, - verbose=verbose, - allow_unsupported=allow_unsupported, - ) - elif backend == "plotext": - return self.plot_plotext( - savefig=savefig, - layers=layers, - verbose=verbose, - ) - elif backend == "tikzfigure": - return self.plot_tikzfigure(savefig=savefig, layers=layers, verbose=verbose) - else: - raise ValueError(f"Invalid backend: {backend}") - def plot(self, *args, backend=None, **kwargs): """Add a line, or render when called with backend options. @@ -2410,12 +2446,13 @@ def show( ) if verbose: print("Displaying Matplotlib figure...") - if _display_matplotlib_figure_in_notebook(fig): - # IPython has rendered the figure already. Closing it prevents - # a later implicit pyplot display and releases its resources. - plt.close(fig) - else: - plt.show(block=block) + with self._matplotlib_style(): + if _display_matplotlib_figure_in_notebook(fig): + # IPython has rendered the figure already. Closing it prevents + # a later implicit pyplot display and releases its resources. + plt.close(fig) + else: + plt.show(block=block) return fig, axes elif backend == "plotly": resolved_usetex = self._usetex if usetex is None else usetex @@ -2449,7 +2486,7 @@ def show( else: raise ValueError("Invalid backend") - def plot_matplotlib( + def _plot_matplotlib( self, savefig: bool = False, layers: list | None = None, @@ -2458,12 +2495,6 @@ def plot_matplotlib( matplotlib_postprocess=None, matplotlib_customizations=None, ): - """ - Generate and optionally display the subplots. - - Parameters: - filename (str, optional): Filename to save the figure. - """ if verbose: print("Generating Matplotlib figure...") @@ -2665,6 +2696,45 @@ def plot_matplotlib( matplotlib_postprocess(fig, axes) return fig, axes + def plot_matplotlib( + self, + savefig: bool = False, + layers: list | None = None, + usetex: bool | None = None, + verbose: bool = False, + matplotlib_postprocess=None, + matplotlib_customizations=None, + ): + """ + Generate and optionally display the subplots. + + The canvas style (fonts, ticks, grid) applies while drawing only: + Matplotlib's global ``rcParams`` are the same afterwards, so figures + drawn later by other code keep their own style. ``savefig`` and + ``show`` apply the style again while saving and showing. + + Parameters: + filename (str, optional): Filename to save the figure. + """ + before = dict(plt.rcParams) + with plt.rc_context(): + result = self._plot_matplotlib( + savefig=savefig, + layers=layers, + usetex=usetex, + verbose=verbose, + matplotlib_postprocess=matplotlib_postprocess, + matplotlib_customizations=matplotlib_customizations, + ) + # what the style changed, for saving and showing the figure later: + # e.g. LaTeX text reads the font family when it is drawn + self._matplotlib_rc = { + key: value + for key, value in plt.rcParams.items() + if key in before and before[key] != value + } + return result + def _validate_import_backend(self, backend, *, allow_unsupported=False): """Never silently drop native imported axes or figure decorations.""" if not hasattr(self, "import_report") or allow_unsupported: @@ -2838,6 +2908,13 @@ def plot_plotly( """ self._validate_import_backend("plotly", allow_unsupported=allow_unsupported) + if any( + isinstance(subplot, TikzFigure) for _, _, subplot in self.iter_subplots() + ): + raise NotImplementedError( + "tikzfigure subplots cannot be rendered with the plotly backend; " + "use the matplotlib or tikzfigure backend" + ) resolved_usetex = self._usetex if usetex is None else usetex for subplot in self._subplot_dict.values(): @@ -2854,16 +2931,17 @@ def plot_plotly( "twiny is currently supported only by the matplotlib backend" ) - setup_tex_fonts( - fontsize=self.fontsize, - usetex=resolved_usetex, - ) # adjust or redefine for Plotly if needed + with plt.rc_context(): # Matplotlib's global style stays as it is + setup_tex_fonts( + fontsize=self.fontsize, + usetex=resolved_usetex, + ) # adjust or redefine for Plotly if needed # Create subplot titles in row-major order (Plotly expects rows*cols entries) subplot_titles = [""] * (self.nrows * self.ncols) for (row, col), sp in self._subplot_dict.items(): index = row * self.ncols + col - subplot_titles[index] = sp._title or f"({row}, {col})" + subplot_titles[index] = sp._title or "" specs = [ [{"secondary_y": (r, c) in self._twinx_subplots} for c in range(self.ncols)] @@ -3116,11 +3194,20 @@ def plot_plotly( col=col + 1, ) - # Update layout settings + # Update layout settings: tight margins, with room for the axis titles. A title needs a + # standoff from its tick labels: with automargin (web figures) and no standoff, Plotly + # pins it to the edge of the figure, half outside. + subplots = self._subplot_dict.values() + has_xlabel = any(getattr(sp, "_xlabel", None) for sp in subplots) + has_ylabel = any(getattr(sp, "_ylabel", None) for sp in subplots) fig.update_layout( font=dict(size=self.fontsize), - margin=dict(l=10, r=10, t=40, b=10), + margin=dict( + l=70 if has_ylabel else 10, r=10, t=40, b=60 if has_xlabel else 10 + ), ) + fig.update_xaxes(title_standoff=8) + fig.update_yaxes(title_standoff=8) barmode = self._barmode if barmode is None: barmode = next( @@ -3159,21 +3246,41 @@ def plot_plotly( self._plotly_fig = fig return fig - def _save_plotly( - self, fig, filename: str, include_plotlyjs: bool | str = True - ) -> None: - _, extension = os.path.splitext(filename) - extension = extension.lower() - if extension in {".html", ".htm"}: - fig.write_html(filename, include_plotlyjs=include_plotlyjs) - return - try: - fig.write_image(filename) - except Exception as exc: - raise RuntimeError( - "Plotly image export failed. For PNG/PDF/SVG export, install kaleido " - "(e.g., `pip install -U kaleido`), or export to HTML instead." - ) from exc + def to_json( + self, + layers: list | None = None, + title: str | None = None, + alt: str | None = None, + white_plot: bool | None = None, + verbose: bool = False, + allow_unsupported: bool = False, + ) -> str: + """Render the canvas with the Plotly backend and return it as a web figure. + + A web figure is theme-neutral Plotly JSON without plotly.js, which the max-models + docs theme embeds on a page with the site's colours (see :mod:`maxplotlib.web`). + ``canvas.savefig("figure.plotly.json", backend="plotly")`` writes the same to a file. + + Parameters: + layers (list, optional): The layers to draw. + title (str, optional): The title and default caption. Default: the canvas title. + alt (str, optional): A text alternative for the figure. + white_plot (bool, optional): Keep a white plot area in dark mode. + """ + import json + + from maxplotlib.web import web_figure + + fig = self.plot_plotly( + show=False, + layers=layers, + verbose=verbose, + allow_unsupported=allow_unsupported, + ) + web = web_figure( + fig, title=title, alt=alt, aspect=self._web_aspect(), white_plot=white_plot + ) + return json.dumps(web, separators=(",", ":")) def to_html( self, diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index da86d4f..b5bc79c 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -323,19 +323,6 @@ def add_line( } self._add(ld, layer) - def plot(self, x, y=None, layer=0, **kwargs): - """Matplotlib-style alias for :meth:`add_line`. - - ``plot(da)`` with a 1-D ``xarray.DataArray`` plots it against its - coordinate; see :meth:`_plot_dataarray`. - """ - if y is None: - if not xarray_support.is_dataarray(x): - raise TypeError("plot(x, y) requires both x and y data") - self._plot_dataarray(self.add_line, x, layer, kwargs) - return - self.add_line(x, y, layer=layer, **kwargs) - def _set_default_labels(self, xlabel, ylabel, da=None): """Set axis labels, and a title from ``da``, that the user has not set.""" if not self._xlabel and xlabel: @@ -381,6 +368,28 @@ def _plot_dataarray(self, method, da, layer, kwargs): if hue is not None and add_legend: self._legend = True + def plot(self, x, y=None, layer=0, **kwargs): + """Matplotlib-style alias for :meth:`add_line`. + + ``plot(da)`` with a 1-D ``xarray.DataArray`` plots it against its + coordinate; see :meth:`_plot_dataarray`. + """ + if y is None: + if not xarray_support.is_dataarray(x): + raise TypeError("plot(x, y) requires both x and y data") + self._plot_dataarray(self.add_line, x, layer, kwargs) + return + self.add_line(x, y, layer=layer, **kwargs) + + @staticmethod + def _mesh_of(da, kwargs, method): + return xarray_support.mesh_data( + da, + x=kwargs.pop("xcoord", None), + y=kwargs.pop("ycoord", None), + method=method, + ) + def _mesh_dataarray(self, method, da, layer, kwargs, add_colorbar=True, name=None): """Draw a 2-D DataArray with ``method(x, y, z, ...)``. @@ -398,15 +407,6 @@ def _mesh_dataarray(self, method, da, layer, kwargs, add_colorbar=True, name=Non if add_colorbar: self.add_colorbar(label=mesh.zlabel, layer=layer) - @staticmethod - def _mesh_of(da, kwargs, method): - return xarray_support.mesh_data( - da, - x=kwargs.pop("xcoord", None), - y=kwargs.pop("ycoord", None), - method=method, - ) - def scatter(self, x, y=None, layer=0, **kwargs): """ Add a scatter plot to the subplot. @@ -1439,6 +1439,9 @@ def text(self, x, y, s, layer=0, **kwargs): } self._add(ld, layer) + def _imshow_xyz(self, x, y, z, layer=0, **kwargs): + self.add_imshow(z, layer=layer, **kwargs) + def add_imshow(self, data, layer=0, **kwargs): """Add an image. @@ -1467,9 +1470,6 @@ def add_imshow(self, data, layer=0, **kwargs): } self._add(ld, layer) - def _imshow_xyz(self, x, y, z, layer=0, **kwargs): - self.add_imshow(z, layer=layer, **kwargs) - def imshow(self, data, layer=0, **kwargs): """Matplotlib-style alias for :meth:`add_imshow`.""" self.add_imshow(data, layer=layer, **kwargs) @@ -1503,6 +1503,35 @@ def layers(self): layers.append(layer_name) return layers + @staticmethod + def _tag_matplotlib_artists(ax, artists_before, meta): + """Set ``gid`` on every artist added since ``artists_before``.""" + if artists_before is None: + return + for artist in ax.get_children(): + if id(artist) in artists_before: + continue + try: + artist.set_gid(str(meta)) + except AttributeError: + continue + + def _shift_x(self, values): + """Apply ``xshift``/``xscale``; values pass through unchanged by default. + + Skipping the no-op arithmetic keeps non-numeric data, such as + datetimes or category labels, plottable. + """ + if self._xshift == 0 and self._xscale == 1: + return values + return (values + self._xshift) * self._xscale + + def _shift_y(self, values): + """Apply ``yshift``/``yscale``; see :meth:`_shift_x`.""" + if self._yshift == 0 and self._yscale == 1: + return values + return (values + self._yshift) * self._yscale + def plot_matplotlib( self, ax, @@ -2031,18 +2060,133 @@ def plot_matplotlib( if index in indicators: ax.indicate_inset_zoom(inset, **indicators[index]) - @staticmethod - def _tag_matplotlib_artists(ax, artists_before, meta): - """Set ``gid`` on every artist added since ``artists_before``.""" - if artists_before is None: + def _symlog_transform(self, values): + array = np.asarray(values, dtype=float) + return np.sign(array) * np.log10(1.0 + np.abs(array)) + + def _plotext_axis_scale(self, axis: str): + return self._xaxis_scale if axis == "x" else self._yaxis_scale + + def _plotext_axis_transform(self, values, axis: str): + array = np.asarray(values) + if axis == "x": + transformed = self._shift_x(array) + else: + transformed = self._shift_y(array) + if self._plotext_axis_scale(axis) == "symlog": + return self._symlog_transform(transformed) + return transformed + + def _transform_x(self, values): + return self._plotext_axis_transform(values, "x") + + def _transform_y(self, values): + return self._plotext_axis_transform(values, "y") + + def _transform_scalar_x(self, value): + return float(np.asarray(self._transform_x([value]))[0]) + + def _transform_scalar_y(self, value): + return float(np.asarray(self._transform_y([value]))[0]) + + def _plotly_shape_corners(self, shape): + """Return the polygon outline of a rectangular shape, if hoverable. + + Shapes anchored to the paper (full-height spans from ``axvspan``, for + instance) have no data coordinates on that axis; they can only be + given a hover overlay when the subplot has explicit limits to + substitute. + """ + if shape.get("type") != "rect": + return None + try: + if shape.get("xref") == "paper": + if self._xmin is None or self._xmax is None: + return None + x0 = self._transform_scalar_x(self._xmin) + x1 = self._transform_scalar_x(self._xmax) + else: + x0 = float(shape["x0"]) + x1 = float(shape["x1"]) + if shape.get("yref") == "paper": + if self._ymin is None or self._ymax is None: + return None + y0 = self._transform_scalar_y(self._ymin) + y1 = self._transform_scalar_y(self._ymax) + else: + y0 = float(shape["y0"]) + y1 = float(shape["y1"]) + except (KeyError, TypeError, ValueError): + return None + return [x0, x1, x1, x0], [y0, y0, y1, y1] + + def _apply_plotly_neutral( + self, + line, + traces, + shapes, + trace_start, + shape_start, + skip_shapes=(), + ): + """Apply the backend-neutral ``hover=`` / ``meta=`` options. + + Every trace and shape produced by ``line`` is tagged, so callers never + have to match traces by creation order after the figure is built. + ``hover`` may be a single string, one entry per point, or a 2-D array + for heatmap-like traces; shapes, which cannot show hover text of their + own, get an invisible overlay trace instead. + """ + hover = line.get("hover") + meta = line.get("meta") + if hover is None and meta is None: return - for artist in ax.get_children(): - if id(artist) in artists_before: + + for trace in traces[trace_start:]: + if meta is not None: + try: + trace.update(meta=meta) + except (ValueError, TypeError): + pass + if hover is not None: + try: + trace.update(hovertext=hover, hoverinfo="text") + except (ValueError, TypeError): + pass + + for index in range(shape_start, len(shapes)): + shape = shapes[index] + if meta is not None and "name" not in shape: + shape["name"] = str(meta) + if hover is None or index in skip_shapes: continue - try: - artist.set_gid(str(meta)) - except AttributeError: + corners = self._plotly_shape_corners(shape) + if corners is None: + continue + x_points, y_points = corners + traces.append( + go.Scatter( + x=list(x_points) + [x_points[0]], + y=list(y_points) + [y_points[0]], + mode="lines", + fill="toself", + fillcolor="rgba(0,0,0,0)", + line=dict(width=0), + hoveron="fills", + hoverinfo="text", + hovertext=hover, + meta=meta, + showlegend=False, + ) + ) + + def _iter_layer_lines(self, layers=None): + for layer_name in sorted(self.layered_line_data): + layer_lines = self.layered_line_data[layer_name] + if layers and layer_name not in layers: continue + for line in layer_lines: + yield line def plot_plotly(self, layers=None, allow_unsupported=False): if hasattr(self, "_import_projection"): @@ -3660,154 +3804,10 @@ def format_bar_label(value): return traces, shapes, annotations - def _apply_plotly_neutral( - self, - line, - traces, - shapes, - trace_start, - shape_start, - skip_shapes=(), - ): - """Apply the backend-neutral ``hover=`` / ``meta=`` options. - - Every trace and shape produced by ``line`` is tagged, so callers never - have to match traces by creation order after the figure is built. - ``hover`` may be a single string, one entry per point, or a 2-D array - for heatmap-like traces; shapes, which cannot show hover text of their - own, get an invisible overlay trace instead. - """ - hover = line.get("hover") - meta = line.get("meta") - if hover is None and meta is None: - return - - for trace in traces[trace_start:]: - if meta is not None: - try: - trace.update(meta=meta) - except (ValueError, TypeError): - pass - if hover is not None: - try: - trace.update(hovertext=hover, hoverinfo="text") - except (ValueError, TypeError): - pass - - for index in range(shape_start, len(shapes)): - shape = shapes[index] - if meta is not None and "name" not in shape: - shape["name"] = str(meta) - if hover is None or index in skip_shapes: - continue - corners = self._plotly_shape_corners(shape) - if corners is None: - continue - x_points, y_points = corners - traces.append( - go.Scatter( - x=list(x_points) + [x_points[0]], - y=list(y_points) + [y_points[0]], - mode="lines", - fill="toself", - fillcolor="rgba(0,0,0,0)", - line=dict(width=0), - hoveron="fills", - hoverinfo="text", - hovertext=hover, - meta=meta, - showlegend=False, - ) - ) - - def _plotly_shape_corners(self, shape): - """Return the polygon outline of a rectangular shape, if hoverable. - - Shapes anchored to the paper (full-height spans from ``axvspan``, for - instance) have no data coordinates on that axis; they can only be - given a hover overlay when the subplot has explicit limits to - substitute. - """ - if shape.get("type") != "rect": - return None - try: - if shape.get("xref") == "paper": - if self._xmin is None or self._xmax is None: - return None - x0 = self._transform_scalar_x(self._xmin) - x1 = self._transform_scalar_x(self._xmax) - else: - x0 = float(shape["x0"]) - x1 = float(shape["x1"]) - if shape.get("yref") == "paper": - if self._ymin is None or self._ymax is None: - return None - y0 = self._transform_scalar_y(self._ymin) - y1 = self._transform_scalar_y(self._ymax) - else: - y0 = float(shape["y0"]) - y1 = float(shape["y1"]) - except (KeyError, TypeError, ValueError): - return None - return [x0, x1, x1, x0], [y0, y0, y1, y1] - - def _iter_layer_lines(self, layers=None): - for layer_name in sorted(self.layered_line_data): - layer_lines = self.layered_line_data[layer_name] - if layers and layer_name not in layers: - continue - for line in layer_lines: - yield line - - def _symlog_transform(self, values): - array = np.asarray(values, dtype=float) - return np.sign(array) * np.log10(1.0 + np.abs(array)) - def _symlog_inverse(self, values): array = np.asarray(values, dtype=float) return np.sign(array) * (10 ** np.abs(array) - 1.0) - def _plotext_axis_scale(self, axis: str): - return self._xaxis_scale if axis == "x" else self._yaxis_scale - - def _shift_x(self, values): - """Apply ``xshift``/``xscale``; values pass through unchanged by default. - - Skipping the no-op arithmetic keeps non-numeric data, such as - datetimes or category labels, plottable. - """ - if self._xshift == 0 and self._xscale == 1: - return values - return (values + self._xshift) * self._xscale - - def _shift_y(self, values): - """Apply ``yshift``/``yscale``; see :meth:`_shift_x`.""" - if self._yshift == 0 and self._yscale == 1: - return values - return (values + self._yshift) * self._yscale - - def _plotext_axis_transform(self, values, axis: str): - array = np.asarray(values) - if axis == "x": - transformed = self._shift_x(array) - else: - transformed = self._shift_y(array) - if self._plotext_axis_scale(axis) == "symlog": - return self._symlog_transform(transformed) - return transformed - - def _transform_x(self, values): - return self._plotext_axis_transform(values, "x") - - def _transform_y(self, values): - return self._plotext_axis_transform(values, "y") - - def _transform_scalar_x(self, value): - return float(np.asarray(self._transform_x([value]))[0]) - - def _transform_scalar_y(self, value): - return float(np.asarray(self._transform_y([value]))[0]) - def _plotext_plot_kwargs(self, kwargs): return { key: kwargs[key] @@ -3930,6 +3930,23 @@ def _coerce_numeric_array(self, values): except (TypeError, ValueError): return None + def _plotext_error_values(self, error, count): + if error is None: + return None + values = np.asarray(error, dtype=float) + if values.ndim == 0: + # Plotext's error() interprets each value as the full bar width, + # while Matplotlib interprets it as the distance from the point + # to one end of a symmetric error bar. + return [float(values) * 2] * count + if values.ndim == 2 and values.shape[0] == 2: + # Matplotlib's asymmetric form is [lower, upper]. Plotext only + # accepts symmetric widths, so preserve the total extent. + values = values[0] + values[1] + else: + values = values * 2 + return values.tolist() + def _plotext_bounds(self, layers=None): xs = [] ys = [] @@ -4011,23 +4028,6 @@ def extend_y(values): extend_y(self._transform_y(patch_ys)) return xs, ys - def _plotext_error_values(self, error, count): - if error is None: - return None - values = np.asarray(error, dtype=float) - if values.ndim == 0: - # Plotext's error() interprets each value as the full bar width, - # while Matplotlib interprets it as the distance from the point - # to one end of a symmetric error bar. - return [float(values) * 2] * count - if values.ndim == 2 and values.shape[0] == 2: - # Matplotlib's asymmetric form is [lower, upper]. Plotext only - # accepts symmetric widths, so preserve the total extent. - values = values[0] + values[1] - else: - values = values * 2 - return values.tolist() - def _plotext_ranges(self, layers=None): xs, ys = self._plotext_bounds(layers=layers) xmin = self._xmin if self._xmin is not None else (min(xs) if xs else None) diff --git a/src/maxplotlib/tests/test_canvas.py b/src/maxplotlib/tests/test_canvas.py index 9d6922d..5379eaa 100644 --- a/src/maxplotlib/tests/test_canvas.py +++ b/src/maxplotlib/tests/test_canvas.py @@ -1323,3 +1323,71 @@ def test_get_tikz_figure_returns_native_figure(): if __name__ == "__main__": test() + + +def test_matplotlib_rendering_keeps_the_global_style(tmp_path): + import matplotlib.pyplot as plt + + from maxplotlib import Canvas + + canvas = Canvas() + canvas.add_subplot().plot([0, 1], [0, 1]) + style = dict(plt.rcParams) + fig, axes = canvas.plot_matplotlib() + # the canvas style applies to its own figure ... + assert axes[0, 0].xaxis.get_ticklabels()[0].get_family() == ["serif"] + assert axes[0, 0].xaxis.get_major_ticks()[0]._tickdir == "in" + # ... and not to Matplotlib's global style, nor to figures drawn later + assert dict(plt.rcParams) == style + other, ax = plt.subplots() + assert ax.xaxis.get_major_ticks()[0]._tickdir == style["xtick.direction"] + plt.close(other) + # saving applies the style again, then restores the global one + canvas.savefig(tmp_path / "canvas.png") + assert (tmp_path / "canvas.png").is_file() + assert dict(plt.rcParams) == style + plt.close(fig) + + +def test_saving_draws_with_the_canvas_style(monkeypatch, tmp_path): + import matplotlib.pyplot as plt + + from maxplotlib import Canvas + + canvas = Canvas() + canvas.add_subplot().plot([0, 1], [0, 1]) + seen = {} + + def fake_savefig(self, *args, **kwargs): + seen["family"] = list(plt.rcParams["font.family"]) + + monkeypatch.setattr(plt.Figure, "savefig", fake_savefig) + canvas.savefig(tmp_path / "canvas.png") + assert seen["family"] == ["serif"] + + +def test_plotly_rendering_keeps_the_global_style(): + import matplotlib.pyplot as plt + import pytest + + pytest.importorskip("plotly") + from maxplotlib import Canvas + + canvas = Canvas() + canvas.add_subplot().plot([0, 1], [0, 1]) + style = dict(plt.rcParams) + canvas.plot_plotly() + assert dict(plt.rcParams) == style + + +def test_plotly_refuses_tikzfigure_subplots(): + import pytest + + pytest.importorskip("plotly") + from maxplotlib import Canvas + + canvas = Canvas() + diagram = canvas.add_tikzfigure() + diagram.add_node(0, 0, label="A") + with pytest.raises(NotImplementedError, match="plotly backend"): + canvas.render(backend="plotly") diff --git a/src/maxplotlib/tests/test_gantt_chart.py b/src/maxplotlib/tests/test_gantt_chart.py index 496e77e..c94057f 100644 --- a/src/maxplotlib/tests/test_gantt_chart.py +++ b/src/maxplotlib/tests/test_gantt_chart.py @@ -169,7 +169,7 @@ def test_gantt_chart_many_tasks(): n_tasks = 20 canvas = Canvas(nrows=1, ncols=1) canvas.gantt( - tasks=[f"Task {i+1}" for i in range(n_tasks)], + tasks=[f"Task {i + 1}" for i in range(n_tasks)], start_times=list(range(0, n_tasks * 2, 2)), durations=[2] * n_tasks, ) diff --git a/src/maxplotlib/tests/test_tikzfigure_backend.py b/src/maxplotlib/tests/test_tikzfigure_backend.py index a496fb8..22e7f5c 100644 --- a/src/maxplotlib/tests/test_tikzfigure_backend.py +++ b/src/maxplotlib/tests/test_tikzfigure_backend.py @@ -107,6 +107,103 @@ def test_text_and_annotation_arrows_are_not_clipped(): assert re.search(r"\\draw\[.*\] \(axis cs:", tikz), "the arrow is a \\draw" +@pytest.mark.parametrize( + "hatch, pattern", + [ + ("///", "north east lines"), + ("\\\\", "north west lines"), + ("x", "crosshatch"), + ("/\\", "crosshatch"), + ("+", "grid"), + ("-", "horizontal lines"), + ("|", "vertical lines"), + ("..", "dots"), + ], +) +def test_hatches_become_tikz_patterns(hatch, pattern): + from matplotlib.patches import Rectangle + + fig, ax = plt.subplots() + ax.add_patch( + Rectangle((0.1, 0.1), 0.5, 0.5, facecolor="0.9", edgecolor="k", hatch=hatch) + ) + ax.add_patch(Rectangle((0.7, 0.7), 0.2, 0.2, facecolor="C0")) # no hatch + figure = figure_to_tikz(fig) + tikz = figure.generate_tikz() + assert tikz.count(f"postaction={{pattern={pattern}, pattern color=mpl000000}}") == 1 + assert "patterns" in figure._tikz_libraries + + +def test_a_patch_with_a_hole_is_one_even_odd_path(): + from matplotlib.patches import Annulus, Rectangle + + fig, ax = plt.subplots() + ax.set_xlim(-2, 2) + ax.set_ylim(-2, 2) + ax.add_patch(Annulus((0, 0), 1.5, 0.5, facecolor="C0", hatch="//", label="ring")) + ax.add_patch(Rectangle((1.6, 1.6), 0.2, 0.2)) # one loop: a plot as before + ax.legend() + tikz = figure_to_tikz(fig).generate_tikz() + ring = [line for line in tikz.splitlines() if "even odd rule" in line] + assert len(ring) == 1 + assert ring[0].count("-- cycle") == 2 # the outer circle and the hole, one path + assert "pattern=north east lines" in ring[0] + assert "\\addlegendentry{ring}" in tikz + assert tikz.count("\\addplot[") == 1 # the rectangle + + +def test_a_hatched_legend_entry_keeps_its_pattern(): + from matplotlib.patches import Rectangle + + fig, ax = plt.subplots() + ax.add_patch(Rectangle((0.1, 0.1), 0.5, 0.5, fill=False, hatch="//", label="wall")) + ax.legend() + tikz = figure_to_tikz(fig).generate_tikz() + assert re.search(r"\\addlegendimage\{[^}]*pattern=north east lines", tikz), tikz + + +def test_straight_arrows_get_tikz_tips(): + from matplotlib.patches import FancyArrowPatch + + fig, ax = plt.subplots() + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.add_patch( + FancyArrowPatch( + (0.1, 0.2), (0.9, 0.2), arrowstyle="<->", mutation_scale=10, shrinkA=0 + ) + ) + ax.add_patch(FancyArrowPatch((0.1, 0.5), (0.9, 0.5), arrowstyle="-|>")) + ax.add_patch( # a curved arrow stays a path + FancyArrowPatch( + (0.1, 0.8), (0.9, 0.8), arrowstyle="->", connectionstyle="arc3,rad=0.3" + ) + ) + ax.text(0.5, 0.9, "⌀ 2 mm") + tikz = figure_to_tikz(fig).generate_tikz() + double = re.search( + r"\\draw\[\{Straight Barb\[length=4pt, width=4pt\]\}-\{Straight Barb\[" + r"length=4pt, width=4pt\]\}, .*shorten >=2pt\] \(axis cs:0\.1,0\.2\) -- " + r"\(axis cs:0\.9,0\.2\);", + tikz, + ) + assert double, tikz + assert re.search(r"\\draw\[-\{Triangle\[length=\d", tikz) + assert tikz.count("\\draw[") == 2 and tikz.count("\\addplot") >= 2 # the curve + assert "{$\\varnothing$ 2 mm}" in tikz + + +def test_an_annotation_arrow_without_text_is_kept(): + fig, ax = plt.subplots() + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.annotate("", xy=(0.9, 0.5), xytext=(0.1, 0.5), arrowprops={"arrowstyle": "<->"}) + ax.text(0.5, 0.5, " ") # blank text draws nothing + tikz = figure_to_tikz(fig).generate_tikz() + assert re.search(r"\\draw\[.*\] \(axis cs:", tikz), "the arrow is a \\draw" + assert "\\node" not in tikz + + def test_far_away_geometry_is_clipped_and_text_outside_the_figure_left_out(): fig, ax = plt.subplots() ax.plot([0, 1e6], [0, 1]) diff --git a/src/maxplotlib/tests/test_web.py b/src/maxplotlib/tests/test_web.py new file mode 100644 index 0000000..e642eb9 --- /dev/null +++ b/src/maxplotlib/tests/test_web.py @@ -0,0 +1,188 @@ +"""Web figures: theme-neutral Plotly JSON for the docs sites.""" + +import json + +import numpy as np +import pytest + +go = pytest.importorskip("plotly.graph_objects") + +from maxplotlib import Canvas # noqa: E402 +from maxplotlib.web import ( # noqa: E402 + FORMAT_VERSION, + color_slot, + web_figure, + write_web_figure, +) + + +def test_color_slot_finds_the_default_cycles(): + assert color_slot("#1f77b4") == 0 # Matplotlib C0 + assert color_slot("#FF7F0E") == 1 # upper case + assert color_slot("rgb(44, 160, 44)") == 2 # C2 as rgb() + assert color_slot("rgba(214, 39, 40, 0.5)") == 3 # C3 with alpha + assert color_slot("#EF553B") == 1 # Plotly's colorway + assert color_slot("#e69f00") == 1 # Okabe-Ito + assert color_slot("#000") == 7 # Okabe-Ito black, short form + assert color_slot("black") is None + assert color_slot("#123456") is None + assert color_slot(["#1f77b4"]) is None # one colour per point is not a slot + + +def test_web_figure_strips_the_look_and_keeps_the_data(): + fig = go.Figure(go.Scatter(x=[0, 1, 2], y=[1, 3, 2], name="a")) + fig.update_layout( + width=800, + height=400, + title="Energy", + paper_bgcolor="white", + template="plotly_dark", + ) + web = web_figure(fig, alt="A rising line") + layout = web["layout"] + assert "template" not in layout + assert "width" not in layout and "height" not in layout + assert layout["paper_bgcolor"] == "rgba(0,0,0,0)" + assert layout["plot_bgcolor"] == "rgba(0,0,0,0)" + assert layout["meta"]["web"] == { + "version": FORMAT_VERSION, + "white_plot": False, + "title": "Energy", + "alt": "A rising line", + "aspect": 2.0, + } + assert web["data"][0]["name"] == "a" + assert "frames" not in web + + +def test_default_colours_get_slots_and_others_stay(): + fig = go.Figure( + [ + go.Scatter(y=[1, 2], line={"color": "#ff7f0e"}), # C1 + go.Scatter(y=[2, 1], line={"color": "black"}, meta="reference"), + go.Scatter( + y=[1, 1], mode="markers", marker={"color": "#2ca02c"}, meta="points" + ), + go.Scatter(y=[0, 1]), # no colour: the page's colorway + ] + ) + data = web_figure(fig)["data"] + assert data[0]["meta"] == {"color_slot": 1} + assert data[1]["meta"] == "reference" + assert data[2]["meta"] == {"value": "points", "color_slot": 2} + assert "meta" not in data[3] + + +def test_a_matplotlib_conversion_keeps_a_white_plot_area_and_its_meta(): + fig = go.Figure( + go.Scatter(y=[1, 2]), layout={"meta": {"converted_from": "matplotlib"}} + ) + layout = web_figure(fig)["layout"] + assert layout["plot_bgcolor"] == "white" + assert layout["meta"]["converted_from"] == "matplotlib" + assert layout["meta"]["web"]["white_plot"] is True + assert ( + web_figure(fig, white_plot=False)["layout"]["plot_bgcolor"] == "rgba(0,0,0,0)" + ) + + +def test_frames_keep_their_slots(): + fig = go.Figure( + go.Scatter(y=[0, 1], line={"color": "#1f77b4"}), + frames=[ + go.Frame(data=[go.Scatter(y=[1, 0], line={"color": "#1f77b4"})], name="1") + ], + ) + web = web_figure(fig) + assert web["frames"][0]["data"][0]["meta"] == {"color_slot": 0} + assert web["frames"][0]["name"] == "1" + + +def test_a_dict_with_arrays_is_accepted(): + figure = { + "data": [{"type": "scatter", "y": np.arange(3)}], + "layout": {"title": {"text": "t"}}, + } + web = web_figure(figure) + assert web["data"][0]["y"] == [0, 1, 2] + assert web["layout"]["meta"]["web"]["title"] == "t" + + +def test_write_web_figure(tmp_path): + path = write_web_figure( + go.Figure(go.Bar(y=[1, 2])), tmp_path / "sub" / "bars.plotly.json", aspect=1.5 + ) + web = json.loads(path.read_text()) + assert web["layout"]["meta"]["web"]["aspect"] == 1.5 + assert web["data"][0]["type"] == "bar" + + +def test_canvas_to_json_and_savefig(tmp_path): + canvas = Canvas(width="10cm", ratio=0.5) + x = np.linspace(0, 1, 5) + canvas.plot(x, x, label="line") + canvas.set_title("A canvas") + web = json.loads(canvas.to_json(alt="A straight line")) + hints = web["layout"]["meta"]["web"] + assert hints["aspect"] == 2.0 + assert hints["alt"] == "A straight line" + assert "template" not in web["layout"] + assert web["layout"]["yaxis"]["automargin"] is True # room for the tick labels + + canvas.savefig(tmp_path / "canvas.plotly.json", backend="plotly") + saved = json.loads((tmp_path / "canvas.plotly.json").read_text()) + assert saved["layout"]["meta"]["web"]["version"] == FORMAT_VERSION + assert saved["data"][0]["name"] == "line" + + +def test_canvas_aspect_from_figsize_and_the_golden_ratio(): + assert Canvas(figsize=(6, 3))._web_aspect() == 2.0 + assert Canvas(width="10cm")._web_aspect() == pytest.approx(1.618, abs=1e-3) + + +def test_plotly_canvas_has_room_for_axis_titles_and_no_placeholder_title(): + canvas = Canvas(width="10cm", ratio=0.5) + canvas.plot([0, 1], [0, 1]) + layout = json.loads(canvas.to_json())["layout"] + texts = [a.get("text") for a in layout.get("annotations", [])] + assert "(0, 0)" not in texts # an untitled subplot has no title + assert layout["margin"]["b"] == 10 and layout["margin"]["l"] == 10 + + canvas.set_xlabel("t") + canvas.set_ylabel("amplitude") + layout = json.loads(canvas.to_json())["layout"] + assert layout["margin"]["b"] == 60 and layout["margin"]["l"] == 70 + assert layout["xaxis"]["title"]["standoff"] == 8 + + +def test_write_posters(tmp_path, monkeypatch): + from maxplotlib.web import write_posters + + written = [] + monkeypatch.setattr( + go.Figure, + "write_image", + lambda self, path, **kw: written.append( + (path, kw, self.layout.font.color, len(self.layout.sliders), self) + ), + ) + fig = go.Figure( + [ + go.Scatter(y=[1, 2], line={"color": "#ff7f0e"}), # C1 + go.Scatter(y=[2, 1], line={"color": "black"}), + ], + layout={"width": 800, "height": 400, "sliders": [{"steps": []}]}, + ) + paths = write_posters(fig, tmp_path / "sub" / "energy") + assert paths == [ + str(tmp_path / "sub" / "energy.png"), + str(tmp_path / "sub" / "energy.dark.png"), + ] + assert written[0][1] == {"width": 800, "height": 400, "scale": 1} + assert (written[0][2], written[1][2]) == ("#10222d", "#eeeadd") + assert written[0][3] == 0 # no slider on a picture + light, dark = written[0][4], written[1][4] + assert light.data[0].line.color == "#c98a00" # C1 in the light palette + assert dark.data[0].line.color == "#f0b030" # and in the dark one + assert dark.data[1].line.color == "#eeeadd" # black as the text colour + assert list(dark.layout.colorway)[0] == "#4fa8e0" diff --git a/src/maxplotlib/utils/xarray_support.py b/src/maxplotlib/utils/xarray_support.py index a36438d..95f9def 100644 --- a/src/maxplotlib/utils/xarray_support.py +++ b/src/maxplotlib/utils/xarray_support.py @@ -220,6 +220,27 @@ def curvilinear(self) -> bool: return np.ndim(self.x) == 2 +def _curvilinear_mesh(da, x, y): + def grid(name): + dims, coord = _resolve(da, name) + if coord is None: + coord = type(da)(np.arange(da.sizes[name]), dims=dims) + return magnitude(coord.broadcast_like(da).transpose(*da.dims)), coord + + xvalues, xcoord = grid(x) + yvalues, ycoord = grid(y) + return MeshData( + xvalues, + yvalues, + magnitude(da), + _label(xcoord) if xcoord.name is not None else str(x), + _label(ycoord) if ycoord.name is not None else str(y), + value_label(da), + x, + y, + ) + + def mesh_data(da, x=None, y=None, method="pcolormesh"): """Return the coordinates, values and labels for a 2-D DataArray. @@ -259,27 +280,6 @@ def mesh_data(da, x=None, y=None, method="pcolormesh"): return MeshData(xvalues, yvalues, z, xlabel, ylabel, value_label(da), x, y) -def _curvilinear_mesh(da, x, y): - def grid(name): - dims, coord = _resolve(da, name) - if coord is None: - coord = type(da)(np.arange(da.sizes[name]), dims=dims) - return magnitude(coord.broadcast_like(da).transpose(*da.dims)), coord - - xvalues, xcoord = grid(x) - yvalues, ycoord = grid(y) - return MeshData( - xvalues, - yvalues, - magnitude(da), - _label(xcoord) if xcoord.name is not None else str(x), - _label(ycoord) if ycoord.name is not None else str(y), - value_label(da), - x, - y, - ) - - def _edges(values, name): """Outer edges of evenly spaced cell centres.""" if values.size == 1: diff --git a/src/maxplotlib/web.py b/src/maxplotlib/web.py new file mode 100644 index 0000000..9aecb2b --- /dev/null +++ b/src/maxplotlib/web.py @@ -0,0 +1,386 @@ +"""Web figures: Plotly figures as theme-neutral JSON for the max-models docs sites. + +A web figure is a Plotly figure saved as JSON (``data``, ``layout``, ``frames``) without +plotly.js and without anything that fixes its look to one page: no template, no fixed width or +height, transparent backgrounds. The ``PlotlyFigure`` component of the max-models docs theme +(starlight-theme) embeds it on a page, loads plotly.js only when the figure scrolls into view, +and gives it the site's colours in light and dark mode: + +- text, grid lines and backgrounds from the theme; +- the theme's data palette for every trace in a default colour. Traces without a colour take it + from ``layout.colorway``, which the page sets to the palette; traces with an explicit colour + from a default cycle (Matplotlib's ``C0``..``C9``, Plotly's default colorway, the Okabe-Ito + palette) carry the index of that colour as ``trace.meta["color_slot"]``, and the page gives + them the palette colour of that index. Black traces take the text colour. Other explicit + colours and colormaps stay as they are. + +Hints for the page are in ``layout.meta["web"]``: + +``version`` + The format version, :data:`FORMAT_VERSION`. +``title`` + The figure's title, also the default caption. +``alt`` + A text alternative for readers who cannot see the figure. +``aspect`` + Width over height, so the page reserves the right space before the figure loads. +``white_plot`` + Keep a white plot area in dark mode too, for colours chosen for a white background + (e.g. a figure converted from Matplotlib with white overlays). + +Examples +-------- +>>> import plotly.graph_objects as go +>>> from maxplotlib.web import write_web_figure +>>> fig = go.Figure(go.Scatter(x=[0, 1, 2], y=[1, 3, 2])) +>>> write_web_figure(fig, "public/figures/line.plotly.json", alt="A rising line") +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +FORMAT_VERSION = 1 + +# The default colour cycles whose colours become palette slots, each in its own order. +MATPLOTLIB_CYCLE = ( + "#1f77b4", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", +) +PLOTLY_COLORWAY = ( + "#636efa", + "#ef553b", + "#00cc96", + "#ab63fa", + "#ffa15a", + "#19d3f3", + "#ff6692", + "#b6e880", + "#ff97ff", + "#fecb52", +) +OKABE_ITO = ( + "#0072b2", + "#e69f00", + "#009e73", + "#d55e00", + "#cc79a7", + "#56b4e9", + "#f0e442", + "#000000", +) +DEFAULT_CYCLES = (MATPLOTLIB_CYCLE, PLOTLY_COLORWAY, OKABE_ITO) + +_RGB = re.compile(r"rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)") +_CARTESIAN_AXIS = re.compile(r"[xy]axis\d*") + + +def _hex(color: Any) -> str | None: + """A colour as ``#rrggbb``, or ``None`` if it is not a single colour string.""" + if not isinstance(color, str): + return None + text = color.strip().lower() + if re.fullmatch(r"#[0-9a-f]{6}", text): + return text + if re.fullmatch(r"#[0-9a-f]{3}", text): + return "#" + "".join(c * 2 for c in text[1:]) + match = _RGB.match(text) + if match: + r, g, b = (round(float(v)) for v in match.groups()) + return f"#{r:02x}{g:02x}{b:02x}" + return None + + +def color_slot(color: Any, cycles=DEFAULT_CYCLES) -> int | None: + """Return the position of a colour in one of the default colour cycles. + + Parameters + ---------- + color : str + A colour, as ``#rrggbb``, ``#rgb``, ``rgb(...)`` or ``rgba(...)``. + cycles : sequence of sequence of str, optional + The cycles to look in, in order. Default: :data:`DEFAULT_CYCLES`. + + Returns + ------- + int or None + The position, or ``None`` if the colour is in none of the cycles. + + Examples + -------- + >>> color_slot("#ff7f0e") # Matplotlib's C1 + 1 + >>> color_slot("black") is None + True + """ + code = _hex(color) + if code is None: + return None + for cycle in cycles: + if code in cycle: + return cycle.index(code) + return None + + +def _trace_slot(trace: dict) -> int | None: + """The palette slot of a trace: of its line colour, else of its marker or fill colour.""" + for path in (("line", "color"), ("marker", "color"), ("fillcolor",)): + value: Any = trace + for key in path: + value = value.get(key) if isinstance(value, dict) else None + slot = color_slot(value) + if slot is not None: + return slot + return None + + +def _json_default(value: Any) -> Any: + tolist = getattr(value, "tolist", None) + if tolist is not None: + return tolist() + raise TypeError(f"cannot write {type(value).__name__} as JSON") + + +def _figure_dict(figure: Any) -> dict: + """The figure as plain JSON data, with Plotly's own encoding of arrays.""" + if isinstance(figure, dict): + return json.loads(json.dumps(figure, default=_json_default)) + import plotly.io as pio + + return json.loads(pio.to_json(figure, validate=False)) + + +def _title_text(layout: dict) -> str | None: + title = layout.get("title") + if isinstance(title, dict): + title = title.get("text") + return title if isinstance(title, str) and title else None + + +def _with_slot(trace: dict) -> dict: + slot = _trace_slot(trace) + if slot is None: + return trace + meta = trace.get("meta") + if meta is not None and not isinstance(meta, dict): + meta = {"value": meta} + return {**trace, "meta": {**(meta or {}), "color_slot": slot}} + + +def web_figure( + figure: Any, + *, + title: str | None = None, + alt: str | None = None, + aspect: float | None = None, + white_plot: bool | None = None, +) -> dict: + """Make a Plotly figure theme-neutral, as a dict ready to write as JSON. + + Parameters + ---------- + figure : plotly.graph_objects.Figure or dict + The figure, or its JSON as a dict (e.g. read from a file). + title : str, optional + The title and default caption. Default: the figure's own title. + alt : str, optional + A text alternative for the figure. Default: none. + aspect : float, optional + Width over height. Default: from the figure's width and height when both are set. + white_plot : bool, optional + Keep a white plot area in dark mode. Default: ``True`` for a figure converted from + Matplotlib (``layout.meta["converted_from"] == "matplotlib"``), else ``False``. + + Returns + ------- + dict + ``data``, ``layout`` and ``frames`` (when the figure has frames) of the web figure. + + Examples + -------- + >>> web = web_figure(fig, alt="Field energy over time") + >>> web["layout"]["meta"]["web"]["version"] + 1 + """ + source = _figure_dict(figure) + layout = dict(source.get("layout") or {}) + meta = layout.get("meta") + meta = ( + dict(meta) + if isinstance(meta, dict) + else ({} if meta is None else {"value": meta}) + ) + + width, height = layout.pop("width", None), layout.pop("height", None) + if aspect is None and width and height: + aspect = float(width) / float(height) + if white_plot is None: + white_plot = meta.get("converted_from") == "matplotlib" + title = title if title is not None else _title_text(layout) + + layout.pop("template", None) + layout["autosize"] = True + # the page sets the size, so make room for tick labels and axis titles at any size + for key, axis in layout.items(): + if _CARTESIAN_AXIS.fullmatch(key) and isinstance(axis, dict): + axis.setdefault("automargin", True) + layout["paper_bgcolor"] = "rgba(0,0,0,0)" + layout["plot_bgcolor"] = "white" if white_plot else "rgba(0,0,0,0)" + hints: dict[str, Any] = {"version": FORMAT_VERSION, "white_plot": bool(white_plot)} + if title: + hints["title"] = title + if alt: + hints["alt"] = alt + if aspect: + hints["aspect"] = round(float(aspect), 4) + meta["web"] = hints + layout["meta"] = meta + + web = { + "data": [_with_slot(trace) for trace in source.get("data") or []], + "layout": layout, + } + frames = source.get("frames") + if frames: + web["frames"] = [ + {**frame, "data": [_with_slot(trace) for trace in frame.get("data") or []]} + for frame in frames + ] + return web + + +def write_web_figure(figure: Any, path: str | Path, **hints: Any) -> Path: + """Write a Plotly figure as a web figure, ``.plotly.json``. + + Parameters + ---------- + figure : plotly.graph_objects.Figure or dict + The figure. + path : str or pathlib.Path + The file; its folder is created when missing. + **hints + ``title``, ``alt``, ``aspect`` and ``white_plot``, as for :func:`web_figure`. + + Returns + ------- + pathlib.Path + The file written. + + Examples + -------- + >>> write_web_figure(fig, "docs/public/figures/energy.plotly.json", alt="Energy over time") + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(web_figure(figure, **hints), separators=(",", ":"))) + return path + + +#: The colours of the posters for light (``""``) and dark (``".dark"``) pages, those of the +#: max-models docs theme: text, grid lines and the data palette (``--mm-data-1`` ... ``-8``). +POSTER_COLORS = { + "": { + "text": "#10222d", + "grid": "rgba(16, 34, 45, 0.12)", + "palette": ("#0072b2", "#c98a00", "#009e73", "#d55e00") + + ("#b55d92", "#3d9fd6", "#a69500", "#5f6b73"), + }, + ".dark": { + "text": "#eeeadd", + "grid": "rgba(238, 234, 221, 0.14)", + "palette": ("#4fa8e0", "#f0b030", "#2fc79a", "#f07a3a") + + ("#e08fc0", "#8fd0f5", "#e8dc4a", "#a8b0b6"), + }, +} + + +def _poster_trace(trace: dict, palette, text: str) -> dict: + """A trace in a poster's colours, as the theme draws it: its palette slot, black as text.""" + slot = ( + (trace.get("meta") or {}).get("color_slot") + if isinstance(trace.get("meta"), dict) + else None + ) + out = dict(trace) + for key in ("line", "marker"): + part = trace.get(key) + if isinstance(part, dict) and isinstance(part.get("color"), str): + if slot is not None: + out[key] = {**part, "color": palette[slot % len(palette)]} + elif ( + part["color"].strip().lower() == "black" + or _hex(part["color"]) == "#000000" + ): + out[key] = {**part, "color": text} + return out + + +def write_posters( + figure: Any, + stem: str | Path, + *, + width: int = 800, + colors: dict | None = None, + **hints: Any, +) -> list[str]: + """Write pictures of a web figure for light and dark pages: ``.png``, ``.dark.png``. + + The docs theme's ``PlotlyFigure`` shows them (``poster``, ``posterDark``) until plotly.js has + drawn the figure, and in its place without JavaScript, e.g. for search engines and link + previews. Both have a transparent background, the theme's text colour and no buttons or + slider; an animation shows its first frame. Needs kaleido. + + Parameters + ---------- + figure : plotly.graph_objects.Figure or dict + The figure. + stem : str or pathlib.Path + The files' path without the extension; the folder is created when missing. + width : int, optional + Width in pixels; the height follows the figure's aspect ratio. Default: 800. + colors : dict, optional + ``{suffix: {"text": ..., "grid": ..., "palette": (...)}}``. Default: :data:`POSTER_COLORS`. + **hints + Passed to :func:`web_figure`, e.g. ``white_plot=False``. + + Returns + ------- + list of str + The paths written. + + Examples + -------- + >>> write_posters(fig, "docs/public/figures/energy") + """ + import plotly.graph_objects as go + + web = web_figure(figure, **hints) + aspect = web["layout"]["meta"]["web"].get("aspect") or 1.6 + Path(stem).parent.mkdir(parents=True, exist_ok=True) + written = [] + for suffix, scheme in (colors or POSTER_COLORS).items(): + text, grid, palette = scheme["text"], scheme["grid"], list(scheme["palette"]) + data = [_poster_trace(trace, palette, text) for trace in web["data"]] + poster = go.Figure({"data": data, "layout": web["layout"]}) + poster.update_layout( + font_color=text, legend_bgcolor="rgba(0,0,0,0)", colorway=palette + ) + poster.layout.updatemenus, poster.layout.sliders = (), () # nothing to press + axes = dict(gridcolor=grid, linecolor=grid, zerolinecolor=grid, mirror=False) + poster.update_xaxes(**axes) + poster.update_yaxes(**axes) + path = f"{stem}{suffix}.png" + poster.write_image(path, width=width, height=round(width / aspect), scale=1) + written.append(path) + return written diff --git a/src/maxplotlib/xarray.py b/src/maxplotlib/xarray.py index e95f4a0..c5d95b7 100644 --- a/src/maxplotlib/xarray.py +++ b/src/maxplotlib/xarray.py @@ -38,23 +38,21 @@ class MaxplotAccessor: def __init__(self, da): self._da = da - def __call__(self, **kwargs): - """Plot with a kind chosen from the dimensions, like ``da.plot()``. - - After removing any ``col``/``row`` dimensions, 1-D data (or 2-D data - with ``hue=``) is drawn with :meth:`line` and 2-D data with - :meth:`pcolormesh`. - """ - facet_dims = {kwargs.get("col"), kwargs.get("row")} - {None} - ndim = self._da.ndim - len(facet_dims & set(self._da.dims)) - if ndim == 1 or (ndim == 2 and "hue" in kwargs): - return self.line(**kwargs) - if ndim == 2: - return self.pcolormesh(**kwargs) - raise ValueError( - f"da.maxplot() plots 1-D or 2-D data, got dims {self._da.dims}. " - "Select a slice first, e.g. da.sel(...), or facet with col=/row=." - ) + def _draw(self, kind, kwargs): + canvas_kwargs = kwargs.pop("canvas_kwargs", None) + # xarray's x=/y= are xcoord=/ycoord= on Canvas methods, where x and y + # are the positional data arguments. + for axis in ("x", "y"): + if axis in kwargs: + kwargs[f"{axis}coord"] = kwargs.pop(axis) + if any(name in kwargs for name in _FACET_KWARGS): + canvas, _ = Canvas.facet( + self._da, kind=kind, canvas_kwargs=canvas_kwargs, **kwargs + ) + return canvas + canvas = Canvas(**(canvas_kwargs or {})) + getattr(canvas, kind)(self._da, **kwargs) + return canvas def line(self, **kwargs): """Lines against the coordinate; ``hue=`` for one per value.""" @@ -80,21 +78,23 @@ def contourf(self, **kwargs): """Filled contours of 2-D data, with a labelled colorbar.""" return self._draw("contourf", kwargs) - def _draw(self, kind, kwargs): - canvas_kwargs = kwargs.pop("canvas_kwargs", None) - # xarray's x=/y= are xcoord=/ycoord= on Canvas methods, where x and y - # are the positional data arguments. - for axis in ("x", "y"): - if axis in kwargs: - kwargs[f"{axis}coord"] = kwargs.pop(axis) - if any(name in kwargs for name in _FACET_KWARGS): - canvas, _ = Canvas.facet( - self._da, kind=kind, canvas_kwargs=canvas_kwargs, **kwargs - ) - return canvas - canvas = Canvas(**(canvas_kwargs or {})) - getattr(canvas, kind)(self._da, **kwargs) - return canvas + def __call__(self, **kwargs): + """Plot with a kind chosen from the dimensions, like ``da.plot()``. + + After removing any ``col``/``row`` dimensions, 1-D data (or 2-D data + with ``hue=``) is drawn with :meth:`line` and 2-D data with + :meth:`pcolormesh`. + """ + facet_dims = {kwargs.get("col"), kwargs.get("row")} - {None} + ndim = self._da.ndim - len(facet_dims & set(self._da.dims)) + if ndim == 1 or (ndim == 2 and "hue" in kwargs): + return self.line(**kwargs) + if ndim == 2: + return self.pcolormesh(**kwargs) + raise ValueError( + f"da.maxplot() plots 1-D or 2-D data, got dims {self._da.dims}. " + "Select a slice first, e.g. da.sel(...), or facet with col=/row=." + ) @xr.register_dataset_accessor("maxplot") @@ -104,27 +104,6 @@ class MaxplotDatasetAccessor: def __init__(self, ds): self._ds = ds - def line(self, x, y, hue=None, **kwargs): - """Lines of variable ``y`` against variable or coordinate ``x``. - - ``hue`` names a dimension to draw one line per value of. Other - arguments are as for ``DataArray.maxplot.line``, including facets. - """ - return self._as_dataarray(x, y).maxplot.line(x=x, hue=hue, **kwargs) - - def scatter(self, x, y, hue=None, **kwargs): - """Points of variable ``y`` against variable or coordinate ``x``. - - ``hue`` names a dimension, for one series per value, or a variable, - which colors the points by value with a labelled colorbar (turned off - by ``add_colorbar=False``; ``cmap``, ``robust`` and ``center`` apply). - """ - if hue is None or hue in self._ds.dims: - return self._as_dataarray(x, y).maxplot.scatter(x=x, hue=hue, **kwargs) - if any(name in kwargs for name in _FACET_KWARGS): - raise ValueError("facets are not supported with a variable as hue=") - return self._scatter_colored(x, y, hue, kwargs) - def _variable(self, name): if name not in self._ds.variables: raise ValueError( @@ -145,6 +124,14 @@ def _as_dataarray(self, x, y): ) return yvar.assign_coords({x: xvar}) + def line(self, x, y, hue=None, **kwargs): + """Lines of variable ``y`` against variable or coordinate ``x``. + + ``hue`` names a dimension to draw one line per value of. Other + arguments are as for ``DataArray.maxplot.line``, including facets. + """ + return self._as_dataarray(x, y).maxplot.line(x=x, hue=hue, **kwargs) + def _scatter_colored(self, x, y, hue, kwargs): canvas_kwargs = kwargs.pop("canvas_kwargs", None) add_colorbar = kwargs.pop("add_colorbar", True) @@ -168,3 +155,16 @@ def _scatter_colored(self, x, y, hue, kwargs): if add_colorbar: canvas.colorbar(label=xarray_support.value_label(cvar)) return canvas + + def scatter(self, x, y, hue=None, **kwargs): + """Points of variable ``y`` against variable or coordinate ``x``. + + ``hue`` names a dimension, for one series per value, or a variable, + which colors the points by value with a labelled colorbar (turned off + by ``add_colorbar=False``; ``cmap``, ``robust`` and ``center`` apply). + """ + if hue is None or hue in self._ds.dims: + return self._as_dataarray(x, y).maxplot.scatter(x=x, hue=hue, **kwargs) + if any(name in kwargs for name in _FACET_KWARGS): + raise ValueError("facets are not supported with a variable as hue=") + return self._scatter_colored(x, y, hue, kwargs) diff --git a/tutorials/tutorial_01.ipynb b/tutorials/tutorial_01.ipynb index 21430b5..1820174 100644 --- a/tutorials/tutorial_01.ipynb +++ b/tutorials/tutorial_01.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 01 — Quick Start\n", + "# Quick start\n", "\n", "**maxplotlib** is a thin, expressive wrapper around Matplotlib that simplifies\n", "creating publication-quality figures. Its central class, `Canvas`, replaces the\n", @@ -25,9 +25,10 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", "import numpy as np\n", "\n", + "from maxplotlib import Canvas\n", + "\n", "%matplotlib inline" ] }, diff --git a/tutorials/tutorial_02.ipynb b/tutorials/tutorial_02.ipynb index 651e27e..0d5ff2d 100644 --- a/tutorials/tutorial_02.ipynb +++ b/tutorials/tutorial_02.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 02 — Multiple Subplots\n", + "# Multiple subplots\n", "\n", "This notebook shows all the ways to build multi-panel figures with maxplotlib:\n", "\n", @@ -24,9 +24,10 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", - "from tikzfigure import TikzFigure\n", "import numpy as np\n", + "from tikzfigure import TikzFigure\n", + "\n", + "from maxplotlib import Canvas\n", "\n", "%matplotlib inline\n", "%load_ext autoreload\n", diff --git a/tutorials/tutorial_03.ipynb b/tutorials/tutorial_03.ipynb index f6b4c18..2320e04 100644 --- a/tutorials/tutorial_03.ipynb +++ b/tutorials/tutorial_03.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 03 — Plot Types\n", + "# Plot types\n", "\n", "maxplotlib exposes all common Matplotlib plot types through a unified API.\n", "This notebook demonstrates each type individually, then shows how to combine\n", @@ -23,9 +23,10 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", "import numpy as np\n", "\n", + "from maxplotlib import Canvas\n", + "\n", "%matplotlib inline\n", "\n", "rng = np.random.default_rng(42)\n", diff --git a/tutorials/tutorial_04.ipynb b/tutorials/tutorial_04.ipynb index 81209c1..5522244 100644 --- a/tutorials/tutorial_04.ipynb +++ b/tutorials/tutorial_04.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 04 – Styling\n", + "# Styling\n", "\n", "maxplotlib passes matplotlib kwargs directly to the underlying renderer, so every colour, linestyle, marker, and alpha option you know from matplotlib works here too." ] @@ -17,8 +17,9 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", - "import numpy as np" + "import numpy as np\n", + "\n", + "from maxplotlib import Canvas" ] }, { diff --git a/tutorials/tutorial_05.ipynb b/tutorials/tutorial_05.ipynb index b7c53bc..b7326f4 100644 --- a/tutorials/tutorial_05.ipynb +++ b/tutorials/tutorial_05.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 05 – Axes, Ticks, Scales, and Annotations\n", + "# Axes and annotations\n", "\n", "This tutorial covers every way to control the coordinate frame of a subplot: labels, limits, tick marks, log scales, grids, legends, and annotations." ] @@ -17,8 +17,9 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", - "import numpy as np" + "import numpy as np\n", + "\n", + "from maxplotlib import Canvas" ] }, { diff --git a/tutorials/tutorial_06.ipynb b/tutorials/tutorial_06.ipynb index 51dc24f..d039e2d 100644 --- a/tutorials/tutorial_06.ipynb +++ b/tutorials/tutorial_06.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 06 – Layers\n", + "# Layers\n", "\n", "**Layers** let you assign each piece of data to a numbered layer. You can then render a subset of layers — e.g. to reveal a derivation step by step, or to produce separate PDF overlays from a single source file." ] @@ -17,8 +17,9 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", - "import numpy as np" + "import numpy as np\n", + "\n", + "from maxplotlib import Canvas" ] }, { diff --git a/tutorials/tutorial_07_tikz.ipynb b/tutorials/tutorial_07_tikz.ipynb index 95f2db0..fafa882 100644 --- a/tutorials/tutorial_07_tikz.ipynb +++ b/tutorials/tutorial_07_tikz.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 07 — TikZ Backend\n", + "# TikZ backend\n", "\n", "**TikZ** is the de-facto standard for drawing in LaTeX documents. \n", "maxplotlib can render your figures as native TikZ code via the `tikzfigure` backend,\n", @@ -36,6 +36,7 @@ "source": [ "import numpy as np\n", "import tikzfigure as tz\n", + "\n", "from maxplotlib import Canvas" ] }, @@ -686,7 +687,7 @@ "upper = np.sin(x_half)\n", "lower = np.cos(x_half)\n", "region = [(float(xi), float(u)) for xi, u in zip(x_half, upper)] + [\n", - " (float(xi), float(l)) for xi, l in zip(reversed(x_half), reversed(lower))\n", + " (float(xi), float(lo)) for xi, lo in zip(reversed(x_half), reversed(lower))\n", "]\n", "tf_final.draw(region, fill=\"cblue!20\", draw=\"none\", cycle=True)\n", "\n", @@ -816,6 +817,7 @@ "outputs": [], "source": [ "import numpy as np\n", + "\n", "from maxplotlib import Canvas\n", "\n", "x = np.arange(5)\n", diff --git a/tutorials/tutorial_08_plotly.ipynb b/tutorials/tutorial_08_plotly.ipynb index 7feb299..ec27496 100644 --- a/tutorials/tutorial_08_plotly.ipynb +++ b/tutorials/tutorial_08_plotly.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 08 – Plotly Backend\n", + "# Plotly backend\n", "\n", "The **Plotly backend** renders your maxplotlib canvas as an interactive `plotly.graph_objects.Figure`. Unlike the default matplotlib output, Plotly figures:\n", "\n", @@ -34,8 +34,9 @@ "%autoreload 2\n", "%matplotlib inline\n", "\n", - "from maxplotlib import Canvas\n", - "import numpy as np" + "import numpy as np\n", + "\n", + "from maxplotlib import Canvas" ] }, { @@ -382,6 +383,132 @@ "cell_type": "markdown", "id": "20", "metadata": {}, + "source": [ + "### Smaller files and embedding\n", + "\n", + "A standalone file bundles plotly.js, several megabytes. `include_plotlyjs` chooses\n", + "where it comes from: `True` (the default of `savefig`) bundles it, `\"cdn\"` loads it\n", + "from a CDN when the page opens, which keeps the file a few kilobytes but needs a\n", + "network connection, and `False` leaves it out for a page that loads plotly.js itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "\n", + "canvas.savefig(\"figure_bundled.html\", backend=\"plotly\")\n", + "canvas.savefig(\"figure_cdn.html\", backend=\"plotly\", include_plotlyjs=\"cdn\")\n", + "for name in (\"figure_bundled.html\", \"figure_cdn.html\"):\n", + " print(f\"{name}: {os.path.getsize(name) / 1024:.0f} kB\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "`canvas.to_html()` returns the HTML as a string instead, with plotly.js from the CDN\n", + "by default. `full_html=False` gives just the `
` and `