diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f19f626 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.idea +.vscode +.claude +.DS_Store +_docs +mmd-cli +mmd-cli-* +*.svg +*.png +*.pdf +compose.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7e49b4e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c6d4353 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Tests + +on: + push: + branches: + - "**" + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Run tests + run: go clean -testcache && go test ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1f890e --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +mmd-cli +/bin/ +/dist/ +*.test +coverage.out +coverage.html +go.work +go.work.sum +.idea/ +.vscode/ +.DS_Store +Thumbs.db +*.svg +*.png +*.pdf +!web/*.js +_docs diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..210a44c --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,32 @@ +version: 2 + +builds: + - main: ./cmd/mmd-cli + binary: mmd-cli + ldflags: + - -s -w -X github.com/coolamit/mermaid-cli/internal/cli.Version={{.Version}} + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + +archives: + - format: tar.gz + name_template: "mmd-cli_{{.Version}}_{{ if eq .Os \"darwin\" }}macos{{ else }}{{ .Os }}{{ end }}_{{ if eq .Arch \"amd64\" }}x64{{ else }}{{ .Arch }}{{ end }}" + +checksum: + name_template: checksums.txt + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + github: + owner: coolamit + name: mermaid-cli diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2567903 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o mmd-cli ./cmd/mmd-cli + +FROM alpine:3.21 + +# Install Chromium +RUN apk add --no-cache \ + chromium \ + nss \ + freetype \ + harfbuzz \ + ca-certificates \ + ttf-freefont \ + font-noto-emoji + +# Tell chromedp where to find the browser +ENV CHROME_BIN=/usr/bin/chromium-browser + +WORKDIR /data + +COPY --from=builder /app/mmd-cli /usr/local/bin/mmd-cli + +ENTRYPOINT ["mmd-cli"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3d3cdd9 --- /dev/null +++ b/Makefile @@ -0,0 +1,109 @@ +# Go build configuration +BINARY_NAME=mmd-cli +GO=/usr/local/go/bin/go +GOFMT=/usr/local/go/bin/gofmt +VERSION?=$(shell cat version) +LDFLAGS=-ldflags '-s -w -X github.com/coolamit/mermaid-cli/internal/cli.Version=$(VERSION)' + +# Declare phony targets +.PHONY: ssh-cmd up down build clean test tidy format build-linux-x64 build-linux-arm64 build-macos-x64 build-macos-arm64 build-all docker-up docker-down docker-run docker-run-aloof docker-clean + +# Common function definitions +define CURRENT_HOMESTEAD_STATUS +cd ../.. && \ +if vagrant status | grep -qE "running \(virtualbox\)|running \(parallels\)"; then echo "running"; else echo "stopped"; fi +endef + +define SSH_EXEC +PROJECT_NAME=$$(basename $$(pwd)) && \ +(cd ../.. && vagrant ssh -- -t "cd code/$$PROJECT_NAME && $(1)") +endef + +# This command will start Homestead if it is not already running. +up: + @echo "Checking Homestead status..." + @STATUS=$$($(CURRENT_HOMESTEAD_STATUS)) && \ + if [ "$$STATUS" = "running" ]; then \ + echo "Homestead is already running."; \ + else \ + echo "Starting Homestead..." && cd ../.. && vagrant up; \ + fi + +# This command will stop Homestead if it is running. +down: + @echo "Checking Homestead status..." + @STATUS=$$($(CURRENT_HOMESTEAD_STATUS)) && \ + if [ "$$STATUS" = "running" ]; then \ + echo "Stopping Homestead..." && cd ../.. && vagrant halt; \ + else \ + echo "Homestead is NOT currently running."; \ + fi + +# Command to allow any command to be run in Homestead VM +# Usage: use -- separator before the command +# Example: make ssh-cmd -- ls -alt +# : make ssh-cmd -- mod tidy +ssh-cmd: + @$(call SSH_EXEC,$(filter-out $@,$(MAKECMDGOALS))) + +# The wildcard rule is needed to allow for artisan or other ssh commands which can have any name. +# This ensures that the catch-all only matches targets that come after +# 'ssh-cmd' in the command line. +ifneq (,$(filter $(firstword $(MAKECMDGOALS)),ssh-cmd docker-run docker-run-aloof)) +%: + @: +endif + +# Go build targets (run inside Homestead VM) +build: + @$(call SSH_EXEC,$(GO) build $(LDFLAGS) -o $(BINARY_NAME) ./cmd/mmd-cli) + @echo "Built $(BINARY_NAME)" + +test: + @$(call SSH_EXEC,$(GO) clean -testcache && $(GO) test ./...) + +tidy: + @$(call SSH_EXEC,$(GO) mod tidy) + +format: + @$(call SSH_EXEC,$(GOFMT) -l -w .) + +clean: + @$(call SSH_EXEC,rm -f $(BINARY_NAME)) + @echo "Cleaned" + +# Cross-compilation targets +build-linux-x64: + @$(call SSH_EXEC,GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BINARY_NAME)-linux-x64 ./cmd/mmd-cli) + +build-linux-arm64: + @$(call SSH_EXEC,GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BINARY_NAME)-linux-arm64 ./cmd/mmd-cli) + +build-macos-x64: + @$(call SSH_EXEC,GOOS=darwin GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BINARY_NAME)-macos-x64 ./cmd/mmd-cli) + +build-macos-arm64: + @$(call SSH_EXEC,GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BINARY_NAME)-macos-arm64 ./cmd/mmd-cli) + +build-all: build-linux-x64 build-linux-arm64 build-macos-x64 build-macos-arm64 + @echo "Built all platforms" + +# Docker Compose targets (run on host, not in Homestead VM) +docker-up: + docker compose up -d + +docker-down: + docker compose stop + +# Run a command in the running Docker container. +# Example: make docker-run -- mmd-cli -i diagram.mmd -o diagram.svg +docker-run: + docker compose exec mmd-cli $(filter-out $@,$(MAKECMDGOALS)) + +# Run mmd-cli in an ephemeral container (no docker-up needed). +# Example: make docker-run-aloof -- -i diagram.mmd -o diagram.svg +docker-run-aloof: + docker compose run --rm --entrypoint mmd-cli mmd-cli $(filter-out $@,$(MAKECMDGOALS)) + +docker-clean: + docker compose down --rmi local --volumes --remove-orphans diff --git a/README.md b/README.md new file mode 100644 index 0000000..80fa342 --- /dev/null +++ b/README.md @@ -0,0 +1,265 @@ +# mmd-cli + +A drop-in replacement for `@mermaid-js/mermaid-cli` written in Go. Produces a single static binary with no Node.js dependency. Requires Chrome or Chromium at runtime. + +Converts Mermaid diagram definitions into SVG, PNG, and PDF files using a headless Chrome browser. + +## Table of Contents + +- [How it Works](#how-it-works) +- [Requirements](#requirements) +- [Installation](#installation) + - [Quick Install](#quick-install) + - [Go Install](#go-install) + - [Manual Download](#manual-download) + - [From Source](#from-source) + - [Cross-Compilation](#cross-compilation) + - [Docker](#docker) +- [Usage](#usage) +- [CLI Flags](#cli-flags) +- [Configuration Files](#configuration-files) + - [Mermaid Config (-c)](#mermaid-config--c) + - [Browser Config (-p)](#browser-config--p) + - [CSS File (-C)](#css-file--c) +- [Docker](#docker-1) + - [Start / Stop](#start--stop) + - [Run Commands](#run-commands) + - [One-off (Ephemeral)](#one-off-ephemeral) + - [Without Docker Compose](#without-docker-compose) +- [License](#license) + +## How it Works + +- Single Go binary with mermaid.js embedded via `go:embed` +- Launches headless Chrome via [chromedp](https://github.com/chromedp/chromedp) (Chrome DevTools Protocol) +- Builds an HTML page with the mermaid diagram definition +- Chrome renders the diagram, then extracts SVG / captures PNG screenshot / prints PDF +- Browser instance is reused across multiple renders for efficiency + +## Requirements + +- Go 1.25+ (for building from source) +- Chrome or Chromium installed on the system (required at runtime) + - On macOS: bundled with the system or install via `brew install --cask chromium` + - On Linux: `apt install chromium-browser` or `apk add chromium` + - Or use the [Docker image](#docker) which bundles Chromium + +## Installation + +### Quick Install + +Detects your OS/architecture, ensures Chrome/Chromium is available, and installs the latest pre-built binary: + +```sh +curl -sSL https://raw.githubusercontent.com/coolamit/mermaid-cli/main/install.sh | sh +``` + +### Go Install + +```sh +go install github.com/coolamit/mermaid-cli/cmd/mmd-cli@latest +``` + +### Manual Download + +Download the archive for your platform from [GitHub Releases](https://github.com/coolamit/mermaid-cli/releases), extract it, and move the `mmd-cli` binary to a directory in your PATH. + +### From Source + +```bash +# Build from source +make build +``` + +### Cross-Compilation + +| Make target | Platform | Architecture | Output binary | +|---|---|---|---| +| `build-linux-x64` | Linux | x64 (Intel/AMD) | `mmd-cli-linux-x64` | +| `build-linux-arm64` | Linux | ARM64 (Apple Silicon, AWS Graviton) | `mmd-cli-linux-arm64` | +| `build-macos-x64` | macOS | x64 (Intel Mac) | `mmd-cli-macos-x64` | +| `build-macos-arm64` | macOS | ARM64 (Apple Silicon) | `mmd-cli-macos-arm64` | + +```bash +# Build for all platforms +make build-all + +# Individual targets +make build-linux-x64 +make build-linux-arm64 +make build-macos-x64 +make build-macos-arm64 +``` + +### Docker + +```bash +# Using Docker Compose (recommended) +make docker-up + +# Or build manually +docker build -t mmd-cli . +``` + +The Docker image pre-bundles Chromium — no local Chrome installation needed. See [Docker usage](#docker-1) for full details. + +## Usage + +```bash +# Basic SVG output +mmd-cli -i diagram.mmd -o diagram.svg + +# PNG output with scale factor +mmd-cli -i diagram.mmd -o diagram.png -s 2 + +# PDF output fitted to content +mmd-cli -i diagram.mmd -o diagram.pdf -f + +# SVG with dimensions matching the diagram size +mmd-cli -i diagram.mmd -o diagram.svg --svgFit + +# Read from stdin, write to stdout +echo "graph TD; A-->B" | mmd-cli -i - -o - -e svg + +# Process markdown file (renders all mermaid blocks) +mmd-cli -i document.md -o output.md + +# Use dark theme +mmd-cli -i diagram.mmd -o diagram.svg -t dark + +# With custom mermaid config +mmd-cli -i diagram.mmd -o diagram.svg -c config.json + +# With browser config +mmd-cli -i diagram.mmd -o diagram.svg -p browser-config.json + +# Transparent background PNG +mmd-cli -i diagram.mmd -o diagram.png -b transparent + +# With icon packs +mmd-cli -i diagram.mmd -o diagram.svg --iconPacks @iconify-json/logos +``` + +## CLI Flags + +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--input` | `-i` | (required) | Input mermaid file. Use `-` for stdin. | +| `--output` | `-o` | `{input}.svg` | Output file. Use `-` for stdout. | +| `--artefacts` | `-a` | output dir | Artefacts output path (markdown mode) | +| `--theme` | `-t` | `default` | Theme: default, forest, dark, neutral | +| `--width` | `-w` | `800` | Page width | +| `--height` | `-H` | `600` | Page height | +| `--backgroundColor` | `-b` | `white` | Background color | +| `--outputFormat` | `-e` | auto | Output format: svg, png, pdf | +| `--scale` | `-s` | `1` | Scale factor | +| `--pdfFit` | `-f` | `false` | Scale PDF to fit chart | +| `--svgFit` | | `false` | Set SVG dimensions to match diagram size | +| `--svgId` | `-I` | | SVG element id attribute | +| `--configFile` | `-c` | | Mermaid JSON config file | +| `--cssFile` | `-C` | | CSS file for styling | +| `--puppeteerConfigFile` | `-p` | | Browser JSON config file | +| `--iconPacks` | | | Icon packs (e.g. @iconify-json/logos) | +| `--iconPacksNamesAndUrls` | | | Icon packs as name#url | +| `--quiet` | `-q` | `false` | Suppress log output | +| `--version` | | | Show version | + +## Configuration Files + +### Mermaid Config (-c) + +JSON file with [mermaid.js configuration options](https://mermaid.js.org/config/schema-docs/config.html). Passed via `--configFile` / `-c`. + +```json +{ + "theme": "forest", + "flowchart": { + "curve": "basis" + }, + "sequence": { + "mirrorActors": false + } +} +``` + +### Browser Config (-p) + +JSON file with browser launch options. Passed via `--puppeteerConfigFile` / `-p`. + +| Field | Type | Description | +|-------|------|-------------| +| `executablePath` | string | Path to Chrome/Chromium binary | +| `args` | string[] | Extra command-line flags for Chrome | +| `timeout` | int | Browser launch timeout (ms) | +| `headless` | string | Headless mode (`"new"` or `"old"`) | + +```json +{ + "executablePath": "/usr/bin/chromium-browser", + "args": ["--no-sandbox", "--disable-gpu"], + "timeout": 30000, + "headless": "new" +} +``` + +### CSS File (-C) + +Custom CSS file applied to the diagram page. Passed via `--cssFile` / `-C`. Useful for custom fonts or overriding default mermaid styles. + +## Docker + +### Start / Stop + +```bash +# Build image (if needed) and start the container in the background +make docker-up + +# Stop the container (preserves it for quick restart) +make docker-down + +# Stop + remove container, image, and volumes +make docker-clean +``` + +### Run Commands + +With the container running (`make docker-up`), exec commands into it: + +```bash +# SVG output +make docker-run -- mmd-cli -i diagram.mmd -o diagram.svg + +# PNG output with scale factor +make docker-run -- mmd-cli -i diagram.mmd -o diagram.png -s 2 + +# PDF output fitted to content +make docker-run -- mmd-cli -i diagram.mmd -o diagram.pdf -f + +# Check version +make docker-run -- mmd-cli --version +``` + +### One-off (Ephemeral) + +Spin up a temporary container, run `mmd-cli`, and destroy it — no `docker-up` needed: + +```bash +make docker-run-aloof -- -i diagram.mmd -o diagram.svg +``` + +### Without Docker Compose + +If you prefer bare `docker run` commands: + +```bash +# Build +docker build -t mmd-cli . + +# SVG output +docker run --rm -v "$(pwd)":/data mmd-cli -i diagram.mmd -o diagram.svg + +# PNG output +docker run --rm -v "$(pwd)":/data mmd-cli -i diagram.mmd -o diagram.png -s 2 +``` + +The Docker image sets `CHROME_BIN=/usr/bin/chromium-browser` and `WORKDIR /data` automatically, so local filenames work directly. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..88f9ede --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + mmd-cli: + build: . + image: mmd-cli + working_dir: /data + volumes: + - .:/data + entrypoint: ["sleep", "infinity"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4093853 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module github.com/coolamit/mermaid-cli + +go 1.25.6 + +require ( + github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d + github.com/chromedp/chromedp v0.14.2 + github.com/spf13/cobra v1.10.2 +) + +require ( + github.com/chromedp/sysutil v1.1.0 // indirect + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + golang.org/x/sys v0.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a7bcff6 --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= +github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= +github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..e45acb7 --- /dev/null +++ b/install.sh @@ -0,0 +1,220 @@ +#!/bin/sh +set -e + +REPO="coolamit/mermaid-cli" +BINARY="mmd-cli" + +# ── Helpers ────────────────────────────────────────────────────────────────── + +info() { printf '\033[1;34m::\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*"; } +error() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; } +die() { error "$*"; exit 1; } + +need() { + command -v "$1" >/dev/null 2>&1 || die "Required tool '$1' not found. Please install it and try again." +} + +# ── Detect OS and architecture ─────────────────────────────────────────────── + +detect_platform() { + OS=$(uname -s) + case "$OS" in + Linux) OS="linux" ;; + Darwin) OS="macos" ;; + *) die "Unsupported operating system: $OS" ;; + esac + + ARCH=$(uname -m) + case "$ARCH" in + x86_64) ARCH="x64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) die "Unsupported architecture: $ARCH" ;; + esac + + info "Detected platform: ${OS}/${ARCH}" +} + +# ── Check installed version ────────────────────────────────────────────────── + +check_installed_version() { + if command -v "$BINARY" >/dev/null 2>&1; then + INSTALLED_VERSION=$("$BINARY" --version 2>/dev/null | awk '{print $NF}' || true) + else + INSTALLED_VERSION="" + fi +} + +# ── Chrome / Chromium check & install ──────────────────────────────────────── + +has_chrome() { + # Check PATH for common binary names + for bin in chromium chromium-browser google-chrome google-chrome-stable; do + command -v "$bin" >/dev/null 2>&1 && return 0 + done + + # Check well-known macOS app locations + [ -d "/Applications/Google Chrome.app" ] && return 0 + [ -d "/Applications/Chromium.app" ] && return 0 + + return 1 +} + +ensure_chrome() { + if has_chrome; then + info "Chrome/Chromium found" + return + fi + + warn "Chrome or Chromium is required but was not found." + + case "$OS" in + linux) + if command -v apt-get >/dev/null 2>&1; then + info "Installing Chromium via apt..." + sudo apt-get update -qq + sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium + elif command -v dnf >/dev/null 2>&1; then + info "Installing Chromium via dnf..." + sudo dnf install -y chromium + elif command -v yum >/dev/null 2>&1; then + info "Installing Chromium via yum..." + sudo yum install -y chromium + else + die "Could not detect a supported package manager. Please install Chrome or Chromium manually." + fi + ;; + macos) + if command -v brew >/dev/null 2>&1; then + info "Installing Chromium via Homebrew..." + brew install --cask chromium + else + echo "" + echo " Please install Chrome or Chromium manually:" + echo " Google Chrome - https://www.google.com/chrome/" + echo " Chromium - https://www.chromium.org/getting-involved/download-chromium/" + echo "" + die "No supported installation method found for Chrome/Chromium on macOS." + fi + ;; + esac + + has_chrome || die "Chrome/Chromium installation appears to have failed. Please install it manually." + info "Chrome/Chromium installed successfully" +} + +# ── Determine version ──────────────────────────────────────────────────────── + +get_version() { + if [ -n "$1" ]; then + VERSION="$1" + # Strip leading 'v' if present for the download URL + VERSION_NUM="${VERSION#v}" + info "Using requested version: ${VERSION_NUM}" + return + fi + + need curl + + info "Fetching latest release version..." + VERSION=$(curl -sSL "https://api.github.com/repos/${REPO}/releases/latest" \ + | grep '"tag_name"' \ + | sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/') + + [ -n "$VERSION" ] || die "Could not determine latest version. Check https://github.com/${REPO}/releases" + + VERSION_NUM="${VERSION#v}" + info "Latest version: ${VERSION_NUM}" +} + +# ── Download and install ───────────────────────────────────────────────────── + +install_binary() { + need curl + need tar + + ARCHIVE="${BINARY}_${VERSION_NUM}_${OS}_${ARCH}.tar.gz" + URL="https://github.com/${REPO}/releases/download/${VERSION}/${ARCHIVE}" + + TMPDIR=$(mktemp -d) + trap 'rm -rf "$TMPDIR"' EXIT + + info "Downloading ${URL}..." + curl -sSL -o "${TMPDIR}/${ARCHIVE}" "$URL" \ + || die "Download failed. Check that version '${VERSION}' exists at https://github.com/${REPO}/releases" + + info "Extracting..." + tar -xzf "${TMPDIR}/${ARCHIVE}" -C "$TMPDIR" + + [ -f "${TMPDIR}/${BINARY}" ] || die "Binary '${BINARY}' not found in archive" + + chmod +x "${TMPDIR}/${BINARY}" + + # Choose install directory + INSTALL_DIR="" + + if [ -d "$HOME/.local/bin" ] || mkdir -p "$HOME/.local/bin" 2>/dev/null; then + INSTALL_DIR="$HOME/.local/bin" + elif [ -w "/usr/local/bin" ]; then + INSTALL_DIR="/usr/local/bin" + else + die "Cannot find a writable install directory. Create ~/.local/bin or run with sudo." + fi + + mv "${TMPDIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" + info "Installed ${BINARY} to ${INSTALL_DIR}/${BINARY}" +} + +# ── Verify installation ───────────────────────────────────────────────────── + +verify() { + if command -v "$BINARY" >/dev/null 2>&1; then + INSTALLED_VERSION=$("$BINARY" --version 2>/dev/null || true) + info "Verified: ${BINARY} ${INSTALLED_VERSION}" + elif [ -x "${INSTALL_DIR}/${BINARY}" ]; then + INSTALLED_VERSION=$("${INSTALL_DIR}/${BINARY}" --version 2>/dev/null || true) + info "Verified: ${BINARY} ${INSTALLED_VERSION}" + + # Check if install dir is in PATH + case ":$PATH:" in + *":${INSTALL_DIR}:"*) ;; + *) + echo "" + warn "${INSTALL_DIR} is not in your PATH." + echo " Add it by running:" + echo "" + echo " export PATH=\"${INSTALL_DIR}:\$PATH\"" + echo "" + echo " To make this permanent, add the line above to your ~/.bashrc, ~/.zshrc, or equivalent." + echo "" + ;; + esac + else + warn "Could not verify installation. You may need to add ${INSTALL_DIR} to your PATH." + fi +} + +# ── Main ───────────────────────────────────────────────────────────────────── + +main() { + detect_platform + check_installed_version + ensure_chrome + get_version "$1" + + if [ -n "$INSTALLED_VERSION" ] && [ "$INSTALLED_VERSION" = "$VERSION_NUM" ]; then + info "${BINARY} ${VERSION_NUM} is already installed and up-to-date." + exit 0 + fi + + if [ -n "$INSTALLED_VERSION" ]; then + info "Updating ${BINARY} from ${INSTALLED_VERSION} to ${VERSION_NUM}..." + fi + + install_binary + verify + + info "Done!" +} + +main "$@" diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..199f338 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,345 @@ +package cli + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/coolamit/mermaid-cli/internal/config" + "github.com/coolamit/mermaid-cli/internal/icons" + "github.com/coolamit/mermaid-cli/internal/markdown" + "github.com/coolamit/mermaid-cli/internal/renderer" + "github.com/spf13/cobra" +) + +// Version is set at build time. +var Version = "dev" + +// Flags holds all CLI flag values. +type Flags struct { + Input string + Output string + Artefacts string + Theme string + Width int + Height int + BackgroundColor string + OutputFormat string + Scale int + PdfFit bool + SvgFit bool + SVGId string + ConfigFile string + CSSFile string + PuppeteerConfigFile string + IconPacks []string + IconPacksNamesAndUrls []string + Quiet bool +} + +// NewRootCommand creates the cobra root command with all flags. +func NewRootCommand() *cobra.Command { + flags := &Flags{} + + cmd := &cobra.Command{ + Use: "mmd-cli", + Short: "Mermaid CLI - Generate diagrams from mermaid definitions", + Long: "A CLI tool to convert mermaid diagram definitions into SVG, PNG, and PDF files.", + Version: Version, + RunE: func(cmd *cobra.Command, args []string) error { + return run(flags) + }, + SilenceUsage: true, + SilenceErrors: true, + } + + // Define flags to match the official mermaid-cli exactly + cmd.Flags().StringVarP(&flags.Input, "input", "i", "", "Input mermaid file. Files ending in .md will be treated as Markdown. Use `-` to read from stdin.") + cmd.Flags().StringVarP(&flags.Output, "output", "o", "", "Output file. It should be either md, svg, png, pdf or use `-` for stdout. Default: input + \".svg\"") + cmd.Flags().StringVarP(&flags.Artefacts, "artefacts", "a", "", "Output artefacts path. Only used with Markdown input.") + cmd.Flags().StringVarP(&flags.Theme, "theme", "t", "default", "Theme of the chart (default, forest, dark, neutral)") + cmd.Flags().IntVarP(&flags.Width, "width", "w", 800, "Width of the page") + cmd.Flags().IntVarP(&flags.Height, "height", "H", 600, "Height of the page") + cmd.Flags().StringVarP(&flags.BackgroundColor, "backgroundColor", "b", "white", "Background color for pngs/svgs (not pdfs). Example: transparent, red, '#F0F0F0'.") + cmd.Flags().StringVarP(&flags.OutputFormat, "outputFormat", "e", "", "Output format for the generated image (svg, png, pdf). Default: from output file extension") + cmd.Flags().IntVarP(&flags.Scale, "scale", "s", 1, "Scale factor") + cmd.Flags().BoolVarP(&flags.PdfFit, "pdfFit", "f", false, "Scale PDF to fit chart") + cmd.Flags().BoolVar(&flags.SvgFit, "svgFit", false, "Set SVG dimensions to match diagram size (for standalone viewing)") + cmd.Flags().StringVarP(&flags.SVGId, "svgId", "I", "", "The id attribute for the SVG element to be rendered") + cmd.Flags().StringVarP(&flags.ConfigFile, "configFile", "c", "", "JSON configuration file for mermaid") + cmd.Flags().StringVarP(&flags.CSSFile, "cssFile", "C", "", "CSS file for the page") + cmd.Flags().StringVarP(&flags.PuppeteerConfigFile, "puppeteerConfigFile", "p", "", "JSON configuration file for the browser") + cmd.Flags().StringSliceVar(&flags.IconPacks, "iconPacks", nil, "Icon packs to use, e.g. @iconify-json/logos") + cmd.Flags().StringSliceVar(&flags.IconPacksNamesAndUrls, "iconPacksNamesAndUrls", nil, "Icon packs with name#url format") + cmd.Flags().BoolVarP(&flags.Quiet, "quiet", "q", false, "Suppress log output") + + return cmd +} + +// info logs a message unless quiet mode is enabled. +func info(quiet bool, format string, args ...interface{}) { + if !quiet { + fmt.Fprintf(os.Stderr, format+"\n", args...) + } +} + +// errorExit prints an error message in red and exits. +func errorExit(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "\033[31m\n%s\n\033[0m", fmt.Sprintf(format, args...)) + os.Exit(1) +} + +func run(flags *Flags) error { + input := flags.Input + output := flags.Output + outputFormat := flags.OutputFormat + quiet := flags.Quiet + + // Validate input + if input == "" { + info(false, "No input file specified, reading from stdin. "+ + "If you want to specify an input file, please use `-i .` "+ + "You can use `-i -` to read from stdin and to suppress this warning.") + } else if input == "-" { + // stdin mode, suppress warning + input = "" + } else if _, err := os.Stat(input); os.IsNotExist(err) { + return fmt.Errorf("input file %q doesn't exist", input) + } + + // Determine output + if output == "" { + if outputFormat != "" { + if input != "" { + output = input + "." + outputFormat + } else { + output = "out." + outputFormat + } + } else { + if input != "" { + output = input + ".svg" + } else { + output = "out.svg" + } + } + } else if output == "-" { + output = "/dev/stdout" + quiet = true + if outputFormat == "" { + outputFormat = "svg" + info(false, "No output format specified, using svg. "+ + "If you want to specify an output format and suppress this warning, "+ + "please use `-e .`") + } + } else { + validExt := regexp.MustCompile(`\.(?:svg|png|pdf|md|markdown)$`) + if !validExt.MatchString(output) { + return fmt.Errorf("output file must end with \".md\"/\".markdown\", \".svg\", \".png\" or \".pdf\"") + } + } + + // Validate artefacts + if flags.Artefacts != "" { + if input == "" || !regexp.MustCompile(`\.(?:md|markdown)$`).MatchString(input) { + return fmt.Errorf("artefacts [-a|--artefacts] path can only be used with Markdown input file") + } + if err := os.MkdirAll(flags.Artefacts, 0755); err != nil { + return fmt.Errorf("failed to create artefacts directory: %w", err) + } + } + + // Check output directory exists + if output != "/dev/stdout" { + outputDir := filepath.Dir(output) + if _, err := os.Stat(outputDir); os.IsNotExist(err) { + return fmt.Errorf("output directory %q/ doesn't exist", outputDir) + } + } + + // Determine output format from extension + if outputFormat == "" { + ext := strings.TrimPrefix(filepath.Ext(output), ".") + if ext == "md" || ext == "markdown" { + outputFormat = "svg" + } else { + outputFormat = ext + } + } + + validFormats := regexp.MustCompile(`^(?:svg|png|pdf)$`) + if !validFormats.MatchString(outputFormat) { + return fmt.Errorf("output format must be one of \"svg\", \"png\" or \"pdf\"") + } + + // Load configs + mermaidConfig, err := config.LoadMermaidConfig(flags.ConfigFile, flags.Theme) + if err != nil { + return err + } + + browserConfig, err := config.LoadBrowserConfig(flags.PuppeteerConfigFile) + if err != nil { + return err + } + + css, err := config.LoadCSSFile(flags.CSSFile) + if err != nil { + return err + } + + // Collect icon packs + var allIconPacks []icons.IconPack + if len(flags.IconPacks) > 0 { + allIconPacks = append(allIconPacks, icons.ParseIconPacks(flags.IconPacks)...) + } + if len(flags.IconPacksNamesAndUrls) > 0 { + allIconPacks = append(allIconPacks, icons.ParseIconPacksNamesAndUrls(flags.IconPacksNamesAndUrls)...) + } + + // Build render options + renderOpts := renderer.RenderOpts{ + MermaidConfig: mermaidConfig, + BackgroundColor: flags.BackgroundColor, + CSS: css, + SVGId: flags.SVGId, + Width: flags.Width, + Height: flags.Height, + Scale: flags.Scale, + PdfFit: flags.PdfFit, + SvgFit: flags.SvgFit, + IconPacks: allIconPacks, + } + + // Read input + var definition string + if input != "" { + data, err := os.ReadFile(input) + if err != nil { + return fmt.Errorf("failed to read input file: %w", err) + } + definition = string(data) + } else { + data, err := readStdin() + if err != nil { + return fmt.Errorf("failed to read stdin: %w", err) + } + definition = string(data) + } + + // Set up renderer + browser := renderer.NewBrowser(browserConfig) + r := renderer.NewRenderer(browser) + defer r.Close() + + ctx := context.Background() + + // Handle markdown input + if input != "" && regexp.MustCompile(`\.(?:md|markdown)$`).MatchString(input) { + if output == "/dev/stdout" { + return fmt.Errorf("cannot use `stdout` with markdown input") + } + + diagrams := markdown.ExtractDiagrams(definition) + + if len(diagrams) > 0 { + info(quiet, "Found %d mermaid charts in Markdown input", len(diagrams)) + } else { + info(quiet, "No mermaid charts found in Markdown input") + } + + imageRefs := make([]markdown.ImageRef, 0, len(diagrams)) + + for _, diagram := range diagrams { + // Build numbered output filename + ext := filepath.Ext(output) + base := strings.TrimSuffix(output, ext) + // If output is .md/.markdown, use outputFormat extension for images + imgExt := ext + if ext == ".md" || ext == ".markdown" { + imgExt = "." + outputFormat + } + outputFile := fmt.Sprintf("%s-%d%s", base, diagram.Index, imgExt) + + if flags.Artefacts != "" { + outputFile = filepath.Join(flags.Artefacts, filepath.Base(outputFile)) + } + + // Calculate relative path from output dir + outputDir := filepath.Dir(filepath.Clean(output)) + relPath, err := filepath.Rel(outputDir, filepath.Clean(outputFile)) + if err != nil { + relPath = outputFile + } + outputFileRelative := "./" + relPath + + result, err := r.Render(ctx, diagram.Definition, outputFormat, renderOpts) + if err != nil { + return fmt.Errorf("failed to render diagram %d: %w", diagram.Index, err) + } + + if err := os.WriteFile(outputFile, result.Data, 0644); err != nil { + return fmt.Errorf("failed to write output file %q: %w", outputFile, err) + } + + info(quiet, " ✅ %s", outputFileRelative) + + imageRefs = append(imageRefs, markdown.ImageRef{ + URL: outputFileRelative, + Alt: result.Desc, + Title: result.Title, + }) + } + + // If output is markdown, replace code blocks with image references + if regexp.MustCompile(`\.(?:md|markdown)$`).MatchString(output) { + outContent := markdown.ReplaceDiagrams(definition, imageRefs) + if err := os.WriteFile(output, []byte(outContent), 0644); err != nil { + return fmt.Errorf("failed to write markdown output: %w", err) + } + info(quiet, " ✅ %s", output) + } + } else { + // Single diagram rendering + info(quiet, "Generating single mermaid chart") + + result, err := r.Render(ctx, definition, outputFormat, renderOpts) + if err != nil { + return err + } + + if output == "/dev/stdout" { + if _, err := os.Stdout.Write(result.Data); err != nil { + return fmt.Errorf("failed to write to stdout: %w", err) + } + } else { + if err := os.WriteFile(output, result.Data, 0644); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + info(quiet, " ✅ %s", output) + } + } + + return nil +} + +// readStdin reads all data from stdin. +func readStdin() ([]byte, error) { + var data []byte + buf := make([]byte, 4096) + for { + n, err := os.Stdin.Read(buf) + if n > 0 { + data = append(data, buf[:n]...) + } + if err != nil { + if err.Error() == "EOF" { + break + } + return nil, err + } + } + return data, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d535d69 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,87 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" +) + +// MermaidConfig holds mermaid.js configuration options. +type MermaidConfig map[string]interface{} + +// BrowserConfig holds browser launch configuration. +type BrowserConfig struct { + ExecutablePath string `json:"executablePath,omitempty"` + Args []string `json:"args,omitempty"` + Timeout int `json:"timeout,omitempty"` + Headless string `json:"headless,omitempty"` +} + +// LoadMermaidConfig reads a mermaid config JSON file and merges it with defaults. +func LoadMermaidConfig(configFile string, theme string) (MermaidConfig, error) { + cfg := MermaidConfig{"theme": theme} + + if configFile == "" { + return cfg, nil + } + + data, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("configuration file %q doesn't exist", configFile) + } + + var fileCfg MermaidConfig + if err := json.Unmarshal(data, &fileCfg); err != nil { + return nil, fmt.Errorf("invalid JSON in config file %q: %w", configFile, err) + } + + // Merge file config over defaults (file takes precedence) + for k, v := range fileCfg { + cfg[k] = v + } + + return cfg, nil +} + +// LoadBrowserConfig reads a browser config JSON file. +func LoadBrowserConfig(configFile string) (*BrowserConfig, error) { + cfg := &BrowserConfig{} + + if configFile == "" { + return cfg, nil + } + + data, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("configuration file %q doesn't exist", configFile) + } + + if err := json.Unmarshal(data, cfg); err != nil { + return nil, fmt.Errorf("invalid JSON in browser config file %q: %w", configFile, err) + } + + return cfg, nil +} + +// LoadCSSFile reads a CSS file and returns its contents. +func LoadCSSFile(cssFile string) (string, error) { + if cssFile == "" { + return "", nil + } + + data, err := os.ReadFile(cssFile) + if err != nil { + return "", fmt.Errorf("CSS file %q doesn't exist", cssFile) + } + + return string(data), nil +} + +// ToJSON serializes a MermaidConfig to JSON string. +func (c MermaidConfig) ToJSON() (string, error) { + data, err := json.Marshal(c) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..8138c5b --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,165 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// --- LoadMermaidConfig --- + +func TestLoadMermaidConfig_EmptyFile(t *testing.T) { + cfg, err := LoadMermaidConfig("", "default") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg["theme"] != "default" { + t.Errorf("expected theme %q, got %q", "default", cfg["theme"]) + } +} + +func TestLoadMermaidConfig_WithFile(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "config.json") + os.WriteFile(p, []byte(`{"theme":"dark","logLevel":"error"}`), 0644) + + cfg, err := LoadMermaidConfig(p, "default") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // File key overrides the default theme + if cfg["theme"] != "dark" { + t.Errorf("expected theme %q, got %q", "dark", cfg["theme"]) + } + if cfg["logLevel"] != "error" { + t.Errorf("expected logLevel %q, got %q", "error", cfg["logLevel"]) + } +} + +func TestLoadMermaidConfig_MissingFile(t *testing.T) { + _, err := LoadMermaidConfig("/nonexistent/config.json", "default") + if err == nil { + t.Fatal("expected error for missing file, got nil") + } +} + +func TestLoadMermaidConfig_InvalidJSON(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "bad.json") + os.WriteFile(p, []byte(`{not json}`), 0644) + + _, err := LoadMermaidConfig(p, "default") + if err == nil { + t.Fatal("expected error for invalid JSON, got nil") + } + if !strings.Contains(err.Error(), "invalid JSON") { + t.Errorf("expected 'invalid JSON' in error, got: %v", err) + } +} + +// --- LoadBrowserConfig --- + +func TestLoadBrowserConfig_EmptyFile(t *testing.T) { + cfg, err := LoadBrowserConfig("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ExecutablePath != "" { + t.Errorf("expected empty ExecutablePath, got %q", cfg.ExecutablePath) + } + if len(cfg.Args) != 0 { + t.Errorf("expected empty Args, got %v", cfg.Args) + } +} + +func TestLoadBrowserConfig_WithFile(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "browser.json") + os.WriteFile(p, []byte(`{"executablePath":"/usr/bin/chromium","args":["--no-sandbox"],"timeout":30000,"headless":"new"}`), 0644) + + cfg, err := LoadBrowserConfig(p) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ExecutablePath != "/usr/bin/chromium" { + t.Errorf("expected executablePath %q, got %q", "/usr/bin/chromium", cfg.ExecutablePath) + } + if len(cfg.Args) != 1 || cfg.Args[0] != "--no-sandbox" { + t.Errorf("expected args [--no-sandbox], got %v", cfg.Args) + } + if cfg.Timeout != 30000 { + t.Errorf("expected timeout 30000, got %d", cfg.Timeout) + } + if cfg.Headless != "new" { + t.Errorf("expected headless %q, got %q", "new", cfg.Headless) + } +} + +func TestLoadBrowserConfig_MissingFile(t *testing.T) { + _, err := LoadBrowserConfig("/nonexistent/browser.json") + if err == nil { + t.Fatal("expected error for missing file, got nil") + } +} + +func TestLoadBrowserConfig_InvalidJSON(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "bad.json") + os.WriteFile(p, []byte(`{nope}`), 0644) + + _, err := LoadBrowserConfig(p) + if err == nil { + t.Fatal("expected error for invalid JSON, got nil") + } + if !strings.Contains(err.Error(), "invalid JSON") { + t.Errorf("expected 'invalid JSON' in error, got: %v", err) + } +} + +// --- LoadCSSFile --- + +func TestLoadCSSFile_Empty(t *testing.T) { + css, err := LoadCSSFile("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if css != "" { + t.Errorf("expected empty string, got %q", css) + } +} + +func TestLoadCSSFile_WithFile(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "style.css") + content := "body { background: red; }\n" + os.WriteFile(p, []byte(content), 0644) + + css, err := LoadCSSFile(p) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if css != content { + t.Errorf("expected %q, got %q", content, css) + } +} + +func TestLoadCSSFile_MissingFile(t *testing.T) { + _, err := LoadCSSFile("/nonexistent/style.css") + if err == nil { + t.Fatal("expected error for missing file, got nil") + } +} + +// --- ToJSON --- + +func TestToJSON(t *testing.T) { + cfg := MermaidConfig{"theme": "forest"} + j, err := cfg.ToJSON() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(j, `"theme":"forest"`) { + t.Errorf("expected JSON to contain theme, got %q", j) + } +} diff --git a/internal/icons/icons.go b/internal/icons/icons.go new file mode 100644 index 0000000..bc2b5f1 --- /dev/null +++ b/internal/icons/icons.go @@ -0,0 +1,60 @@ +package icons + +import ( + "fmt" + "strings" +) + +// IconPack represents an icon pack with a name and loader URL. +type IconPack struct { + Name string + URL string +} + +// ParseIconPacks parses --iconPacks flags into IconPack structs. +// Format: @iconify-json/logos -> name=logos, url=https://unpkg.com/@iconify-json/logos/icons.json +func ParseIconPacks(packs []string) []IconPack { + result := make([]IconPack, 0, len(packs)) + for _, pack := range packs { + parts := strings.Split(pack, "/") + name := parts[len(parts)-1] + url := fmt.Sprintf("https://unpkg.com/%s/icons.json", pack) + result = append(result, IconPack{Name: name, URL: url}) + } + return result +} + +// ParseIconPacksNamesAndUrls parses --iconPacksNamesAndUrls flags. +// Format: name#url +func ParseIconPacksNamesAndUrls(packs []string) []IconPack { + result := make([]IconPack, 0, len(packs)) + for _, pack := range packs { + idx := strings.Index(pack, "#") + if idx < 0 { + continue + } + name := pack[:idx] + url := pack[idx+1:] + result = append(result, IconPack{Name: name, URL: url}) + } + return result +} + +// GenerateIconPackJS generates JavaScript code to register icon packs with mermaid. +func GenerateIconPackJS(packs []IconPack) string { + if len(packs) == 0 { + return "" + } + + var sb strings.Builder + sb.WriteString("mermaid.registerIconPacks([\n") + for _, pack := range packs { + sb.WriteString(fmt.Sprintf(` { + name: %q, + loader: () => fetch(%q).then((res) => res.json()).catch(() => console.error("Failed to fetch icon: %s")) + }, +`, pack.Name, pack.URL, pack.Name)) + } + sb.WriteString("]);\n") + return sb.String() +} diff --git a/internal/icons/icons_test.go b/internal/icons/icons_test.go new file mode 100644 index 0000000..c92ba2d --- /dev/null +++ b/internal/icons/icons_test.go @@ -0,0 +1,110 @@ +package icons + +import ( + "strings" + "testing" +) + +// --- ParseIconPacks --- + +func TestParseIconPacks(t *testing.T) { + packs := ParseIconPacks([]string{"@iconify-json/logos"}) + if len(packs) != 1 { + t.Fatalf("expected 1 pack, got %d", len(packs)) + } + if packs[0].Name != "logos" { + t.Errorf("expected name %q, got %q", "logos", packs[0].Name) + } + want := "https://unpkg.com/@iconify-json/logos/icons.json" + if packs[0].URL != want { + t.Errorf("expected URL %q, got %q", want, packs[0].URL) + } +} + +func TestParseIconPacks_Multiple(t *testing.T) { + packs := ParseIconPacks([]string{"@iconify-json/logos", "@iconify-json/mdi"}) + if len(packs) != 2 { + t.Fatalf("expected 2 packs, got %d", len(packs)) + } + if packs[0].Name != "logos" { + t.Errorf("expected first name %q, got %q", "logos", packs[0].Name) + } + if packs[1].Name != "mdi" { + t.Errorf("expected second name %q, got %q", "mdi", packs[1].Name) + } +} + +func TestParseIconPacks_Empty(t *testing.T) { + packs := ParseIconPacks([]string{}) + if len(packs) != 0 { + t.Errorf("expected 0 packs, got %d", len(packs)) + } +} + +// --- ParseIconPacksNamesAndUrls --- + +func TestParseIconPacksNamesAndUrls(t *testing.T) { + packs := ParseIconPacksNamesAndUrls([]string{"myicons#https://example.com/icons.json"}) + if len(packs) != 1 { + t.Fatalf("expected 1 pack, got %d", len(packs)) + } + if packs[0].Name != "myicons" { + t.Errorf("expected name %q, got %q", "myicons", packs[0].Name) + } + if packs[0].URL != "https://example.com/icons.json" { + t.Errorf("expected URL %q, got %q", "https://example.com/icons.json", packs[0].URL) + } +} + +func TestParseIconPacksNamesAndUrls_NoHash(t *testing.T) { + packs := ParseIconPacksNamesAndUrls([]string{"no-hash-here"}) + if len(packs) != 0 { + t.Errorf("expected 0 packs for entries without #, got %d", len(packs)) + } +} + +func TestParseIconPacksNamesAndUrls_Empty(t *testing.T) { + packs := ParseIconPacksNamesAndUrls([]string{}) + if len(packs) != 0 { + t.Errorf("expected 0 packs, got %d", len(packs)) + } +} + +// --- GenerateIconPackJS --- + +func TestGenerateIconPackJS_Empty(t *testing.T) { + js := GenerateIconPackJS([]IconPack{}) + if js != "" { + t.Errorf("expected empty string, got %q", js) + } +} + +func TestGenerateIconPackJS_Single(t *testing.T) { + packs := []IconPack{{Name: "logos", URL: "https://example.com/logos.json"}} + js := GenerateIconPackJS(packs) + + if !strings.Contains(js, "mermaid.registerIconPacks") { + t.Error("expected output to contain mermaid.registerIconPacks") + } + if !strings.Contains(js, `"logos"`) { + t.Error("expected output to contain pack name") + } + if !strings.Contains(js, `"https://example.com/logos.json"`) { + t.Error("expected output to contain pack URL") + } +} + +func TestGenerateIconPackJS_Multiple(t *testing.T) { + packs := []IconPack{ + {Name: "logos", URL: "https://example.com/logos.json"}, + {Name: "mdi", URL: "https://example.com/mdi.json"}, + } + js := GenerateIconPackJS(packs) + + if !strings.Contains(js, `"logos"`) { + t.Error("expected output to contain first pack name") + } + if !strings.Contains(js, `"mdi"`) { + t.Error("expected output to contain second pack name") + } +} diff --git a/internal/markdown/markdown.go b/internal/markdown/markdown.go new file mode 100644 index 0000000..bcfddb6 --- /dev/null +++ b/internal/markdown/markdown.go @@ -0,0 +1,88 @@ +package markdown + +import ( + "fmt" + "regexp" + "strings" +) + +// mermaidBlockRegex matches ```mermaid ... ``` and :::mermaid ... ::: code blocks. +// Mirrors the official CLI regex: /^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/gm +var mermaidBlockRegex = regexp.MustCompile(`(?m)^[^\S\n]*[\x60:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[\x60:]{3}[^\S\n]*$`) + +// DiagramBlock represents a mermaid diagram found in markdown. +type DiagramBlock struct { + // FullMatch is the entire matched text including fences + FullMatch string + // Definition is the mermaid diagram definition (inner content) + Definition string + // Index is the 1-based index of this diagram in the markdown + Index int +} + +// ExtractDiagrams finds all mermaid code blocks in markdown content. +func ExtractDiagrams(content string) []DiagramBlock { + matches := mermaidBlockRegex.FindAllStringSubmatch(content, -1) + blocks := make([]DiagramBlock, 0, len(matches)) + + for i, match := range matches { + blocks = append(blocks, DiagramBlock{ + FullMatch: match[0], + Definition: strings.TrimSpace(match[2]), + Index: i + 1, + }) + } + + return blocks +} + +// ImageRef holds information about a rendered diagram image. +type ImageRef struct { + URL string + Alt string + Title string +} + +// MarkdownImage creates a markdown image reference: ![alt](url "title") +func MarkdownImage(ref ImageRef) string { + alt := escapeMarkdownAlt(ref.Alt) + if alt == "" { + alt = "diagram" + } + + if ref.Title != "" { + title := escapeMarkdownTitle(ref.Title) + return fmt.Sprintf("![%s](%s \"%s\")", alt, ref.URL, title) + } + return fmt.Sprintf("![%s](%s)", alt, ref.URL) +} + +// ReplaceDiagrams replaces mermaid code blocks in markdown with image references. +func ReplaceDiagrams(content string, images []ImageRef) string { + idx := 0 + return mermaidBlockRegex.ReplaceAllStringFunc(content, func(match string) string { + if idx >= len(images) { + return match + } + img := images[idx] + idx++ + return MarkdownImage(img) + }) +} + +func escapeMarkdownAlt(s string) string { + replacer := strings.NewReplacer( + "[", "\\[", + "]", "\\]", + "\\", "\\\\", + ) + return replacer.Replace(s) +} + +func escapeMarkdownTitle(s string) string { + replacer := strings.NewReplacer( + `"`, `\"`, + `\`, `\\`, + ) + return replacer.Replace(s) +} diff --git a/internal/markdown/markdown_test.go b/internal/markdown/markdown_test.go new file mode 100644 index 0000000..44f64bc --- /dev/null +++ b/internal/markdown/markdown_test.go @@ -0,0 +1,169 @@ +package markdown + +import ( + "strings" + "testing" +) + +// --- ExtractDiagrams --- + +func TestExtractDiagrams_Backtick(t *testing.T) { + md := "```mermaid\ngraph TD;\n A-->B;\n```" + blocks := ExtractDiagrams(md) + if len(blocks) != 1 { + t.Fatalf("expected 1 block, got %d", len(blocks)) + } + if !strings.Contains(blocks[0].Definition, "A-->B") { + t.Errorf("expected definition to contain 'A-->B', got %q", blocks[0].Definition) + } + if blocks[0].Index != 1 { + t.Errorf("expected Index 1, got %d", blocks[0].Index) + } +} + +func TestExtractDiagrams_Colon(t *testing.T) { + md := ":::mermaid\ngraph TD;\n A-->B;\n:::" + blocks := ExtractDiagrams(md) + if len(blocks) != 1 { + t.Fatalf("expected 1 block, got %d", len(blocks)) + } + if !strings.Contains(blocks[0].Definition, "A-->B") { + t.Errorf("expected definition to contain 'A-->B', got %q", blocks[0].Definition) + } +} + +func TestExtractDiagrams_Multiple(t *testing.T) { + md := "```mermaid\ngraph TD;\n A-->B;\n```\n\nSome text\n\n```mermaid\nsequenceDiagram\n Alice->>Bob: Hi\n```" + blocks := ExtractDiagrams(md) + if len(blocks) != 2 { + t.Fatalf("expected 2 blocks, got %d", len(blocks)) + } + if blocks[0].Index != 1 { + t.Errorf("expected first Index 1, got %d", blocks[0].Index) + } + if blocks[1].Index != 2 { + t.Errorf("expected second Index 2, got %d", blocks[1].Index) + } + if !strings.Contains(blocks[1].Definition, "sequenceDiagram") { + t.Errorf("expected second definition to contain 'sequenceDiagram', got %q", blocks[1].Definition) + } +} + +func TestExtractDiagrams_None(t *testing.T) { + md := "# Just a heading\n\nSome regular markdown." + blocks := ExtractDiagrams(md) + if len(blocks) != 0 { + t.Errorf("expected 0 blocks, got %d", len(blocks)) + } +} + +func TestExtractDiagrams_MixedContent(t *testing.T) { + md := `# Title + +Some paragraph. + +` + "```mermaid\ngraph LR;\n X-->Y;\n```" + ` + +More text here. + +` + "```go\nfunc main() {}\n```" + ` + +Final paragraph. +` + blocks := ExtractDiagrams(md) + if len(blocks) != 1 { + t.Fatalf("expected 1 mermaid block among mixed content, got %d", len(blocks)) + } + if !strings.Contains(blocks[0].Definition, "X-->Y") { + t.Errorf("expected definition to contain 'X-->Y', got %q", blocks[0].Definition) + } +} + +// --- MarkdownImage --- + +func TestMarkdownImage_Basic(t *testing.T) { + img := MarkdownImage(ImageRef{URL: "diagram.png", Alt: "My Diagram"}) + want := "![My Diagram](diagram.png)" + if img != want { + t.Errorf("expected %q, got %q", want, img) + } +} + +func TestMarkdownImage_WithTitle(t *testing.T) { + img := MarkdownImage(ImageRef{URL: "diagram.png", Alt: "My Diagram", Title: "A title"}) + want := `![My Diagram](diagram.png "A title")` + if img != want { + t.Errorf("expected %q, got %q", want, img) + } +} + +func TestMarkdownImage_EmptyAlt(t *testing.T) { + img := MarkdownImage(ImageRef{URL: "diagram.png"}) + want := "![diagram](diagram.png)" + if img != want { + t.Errorf("expected %q, got %q", want, img) + } +} + +func TestMarkdownImage_SpecialChars(t *testing.T) { + img := MarkdownImage(ImageRef{ + URL: "diagram.png", + Alt: "diagram [1]", + Title: `say "hello"`, + }) + if !strings.Contains(img, `\[1\]`) { + t.Errorf("expected brackets to be escaped in alt, got %q", img) + } + if !strings.Contains(img, `\"hello\"`) { + t.Errorf("expected quotes to be escaped in title, got %q", img) + } +} + +// --- ReplaceDiagrams --- + +func TestReplaceDiagrams(t *testing.T) { + md := "Before\n\n```mermaid\ngraph TD;\n A-->B;\n```\n\nAfter" + images := []ImageRef{{URL: "out.png", Alt: "Diagram 1"}} + result := ReplaceDiagrams(md, images) + + if strings.Contains(result, "```mermaid") { + t.Error("expected mermaid block to be replaced") + } + if !strings.Contains(result, "![Diagram 1](out.png)") { + t.Errorf("expected image reference in output, got %q", result) + } + if !strings.Contains(result, "Before") || !strings.Contains(result, "After") { + t.Error("expected surrounding text to be preserved") + } +} + +func TestReplaceDiagrams_MoreImagesThanBlocks(t *testing.T) { + md := "```mermaid\ngraph TD;\n A-->B;\n```" + images := []ImageRef{ + {URL: "first.png", Alt: "First"}, + {URL: "extra.png", Alt: "Extra"}, + } + result := ReplaceDiagrams(md, images) + + if !strings.Contains(result, "![First](first.png)") { + t.Errorf("expected first image, got %q", result) + } + // Extra image is simply ignored + if strings.Contains(result, "extra.png") { + t.Error("extra image should not appear in output") + } +} + +func TestReplaceDiagrams_FewerImagesThanBlocks(t *testing.T) { + md := "```mermaid\ngraph TD;\n A-->B;\n```\n\n```mermaid\nsequenceDiagram\n Alice->>Bob: Hi\n```" + images := []ImageRef{{URL: "first.png", Alt: "First"}} + result := ReplaceDiagrams(md, images) + + if !strings.Contains(result, "![First](first.png)") { + t.Errorf("expected first block replaced, got %q", result) + } + // Second block should remain as-is + if !strings.Contains(result, "```mermaid") { + t.Error("expected unmatched mermaid block to be left as-is") + } +} diff --git a/internal/renderer/browser.go b/internal/renderer/browser.go new file mode 100644 index 0000000..6ff2c7d --- /dev/null +++ b/internal/renderer/browser.go @@ -0,0 +1,83 @@ +package renderer + +import ( + "context" + "sync" + + "github.com/chromedp/chromedp" + "github.com/coolamit/mermaid-cli/internal/config" +) + +// Browser manages a lazy-started headless Chrome instance that is reused across renders. +type Browser struct { + mu sync.Mutex + allocCtx context.Context + allocCancel context.CancelFunc + browserCtx context.Context + browserCancel context.CancelFunc + started bool + cfg *config.BrowserConfig +} + +// NewBrowser creates a new Browser manager with the given config. +func NewBrowser(cfg *config.BrowserConfig) *Browser { + if cfg == nil { + cfg = &config.BrowserConfig{} + } + return &Browser{cfg: cfg} +} + +// Context returns a chromedp context, lazily starting the browser if needed. +func (b *Browser) Context(ctx context.Context) (context.Context, error) { + b.mu.Lock() + defer b.mu.Unlock() + + if b.started { + return b.browserCtx, nil + } + + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.Flag("disable-setuid-sandbox", true), + ) + + if b.cfg.ExecutablePath != "" { + opts = append(opts, chromedp.ExecPath(b.cfg.ExecutablePath)) + } + + for _, arg := range b.cfg.Args { + opts = append(opts, chromedp.Flag(arg, true)) + } + + b.allocCtx, b.allocCancel = chromedp.NewExecAllocator(ctx, opts...) + b.browserCtx, b.browserCancel = chromedp.NewContext(b.allocCtx) + + // Run a no-op to force the browser to start + if err := chromedp.Run(b.browserCtx); err != nil { + b.allocCancel() + return nil, err + } + + b.started = true + return b.browserCtx, nil +} + +// Close shuts down the browser. +func (b *Browser) Close() { + b.mu.Lock() + defer b.mu.Unlock() + + if !b.started { + return + } + + if b.browserCancel != nil { + b.browserCancel() + } + if b.allocCancel != nil { + b.allocCancel() + } + b.started = false +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go new file mode 100644 index 0000000..5285ff0 --- /dev/null +++ b/internal/renderer/renderer.go @@ -0,0 +1,357 @@ +package renderer + +import ( + "context" + "encoding/json" + "fmt" + "math" + "time" + + "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/emulation" + "github.com/chromedp/cdproto/page" + "github.com/chromedp/chromedp" +) + +// RenderResult contains the output of rendering a mermaid diagram. +type RenderResult struct { + Data []byte + Title string + Desc string +} + +// Renderer handles mermaid diagram rendering via chromedp. +type Renderer struct { + browser *Browser +} + +// NewRenderer creates a new Renderer with the given browser. +func NewRenderer(browser *Browser) *Renderer { + return &Renderer{browser: browser} +} + +// Render renders a mermaid diagram to the specified output format. +func (r *Renderer) Render(ctx context.Context, definition string, outputFormat string, opts RenderOpts) (*RenderResult, error) { + browserCtx, err := r.browser.Context(ctx) + if err != nil { + return nil, fmt.Errorf("failed to start browser: %w", err) + } + + // Create a new tab + tabCtx, tabCancel := chromedp.NewContext(browserCtx) + defer tabCancel() + + // Set timeout + tabCtx, timeoutCancel := context.WithTimeout(tabCtx, 60*time.Second) + defer timeoutCancel() + + // Build the HTML page + pageHTML, err := BuildPageHTML(definition, opts) + if err != nil { + return nil, fmt.Errorf("failed to build page HTML: %w", err) + } + + // Set viewport + if err := chromedp.Run(tabCtx, + emulation.SetDeviceMetricsOverride(int64(opts.Width), int64(opts.Height), float64(opts.Scale), false), + ); err != nil { + return nil, fmt.Errorf("failed to set viewport: %w", err) + } + + // Navigate to about:blank, then set the HTML content via CDP + var frameTree *page.FrameTree + if err := chromedp.Run(tabCtx, + chromedp.Navigate("about:blank"), + chromedp.ActionFunc(func(ctx context.Context) error { + var err error + frameTree, err = page.GetFrameTree().Do(ctx) + return err + }), + ); err != nil { + return nil, fmt.Errorf("failed to navigate: %w", err) + } + + if err := chromedp.Run(tabCtx, chromedp.ActionFunc(func(ctx context.Context) error { + return page.SetDocumentContent(frameTree.Frame.ID, pageHTML).Do(ctx) + })); err != nil { + return nil, fmt.Errorf("failed to set page content: %w", err) + } + + // Wait for rendering to complete + if err := chromedp.Run(tabCtx, + chromedp.WaitReady("#container svg", chromedp.ByQuery), + ); err != nil { + // Check if there was a render error + var resultJSON string + _ = chromedp.Run(tabCtx, + chromedp.Evaluate(`JSON.stringify(window.__mmd_result || {})`, &resultJSON), + ) + return nil, fmt.Errorf("mermaid rendering failed (waited for SVG): %w\nrender result: %s", err, resultJSON) + } + + // Check for errors in the render result + var resultJSON string + if err := chromedp.Run(tabCtx, + chromedp.Evaluate(`JSON.stringify(window.__mmd_result || {})`, &resultJSON), + ); err != nil { + return nil, fmt.Errorf("failed to get render result: %w", err) + } + + var renderResult struct { + Title *string `json:"title"` + Desc *string `json:"desc"` + Success bool `json:"success"` + Error string `json:"error"` + } + if err := json.Unmarshal([]byte(resultJSON), &renderResult); err != nil { + return nil, fmt.Errorf("failed to parse render result: %w", err) + } + if !renderResult.Success { + return nil, fmt.Errorf("mermaid rendering error: %s", renderResult.Error) + } + + result := &RenderResult{} + if renderResult.Title != nil { + result.Title = *renderResult.Title + } + if renderResult.Desc != nil { + result.Desc = *renderResult.Desc + } + + switch outputFormat { + case "svg": + var data []byte + var err error + if opts.SvgFit { + data, err = extractSVGFit(tabCtx) + } else { + data, err = extractSVG(tabCtx) + } + if err != nil { + return nil, err + } + result.Data = data + + case "png": + data, err := capturePNG(tabCtx, opts) + if err != nil { + return nil, err + } + result.Data = data + + case "pdf": + data, err := capturePDF(tabCtx, opts) + if err != nil { + return nil, err + } + result.Data = data + + default: + return nil, fmt.Errorf("unsupported output format: %s", outputFormat) + } + + return result, nil +} + +// Close closes the browser. +func (r *Renderer) Close() { + r.browser.Close() +} + +// extractSVG extracts the SVG XML from the page using XMLSerializer. +func extractSVG(ctx context.Context) ([]byte, error) { + var svgXML string + err := chromedp.Run(ctx, + chromedp.Evaluate(`(() => { + const svg = document.querySelector('#container svg'); + if (!svg) return ''; + const serializer = new XMLSerializer(); + return serializer.serializeToString(svg); + })()`, &svgXML), + ) + if err != nil { + return nil, fmt.Errorf("failed to extract SVG: %w", err) + } + if svgXML == "" { + return nil, fmt.Errorf("no SVG element found in rendered output") + } + return []byte(svgXML), nil +} + +// extractSVGFit extracts the SVG XML with dimensions set to match the viewBox (for standalone viewing). +func extractSVGFit(ctx context.Context) ([]byte, error) { + var svgXML string + err := chromedp.Run(ctx, + chromedp.Evaluate(`(() => { + const svg = document.querySelector('#container svg'); + if (!svg) return ''; + const viewBox = svg.getAttribute('viewBox'); + if (viewBox) { + const parts = viewBox.split(/\s+/); + if (parts.length === 4) { + svg.setAttribute('width', parts[2]); + svg.setAttribute('height', parts[3]); + svg.style.removeProperty('max-width'); + } + } + const serializer = new XMLSerializer(); + return serializer.serializeToString(svg); + })()`, &svgXML), + ) + if err != nil { + return nil, fmt.Errorf("failed to extract SVG: %w", err) + } + if svgXML == "" { + return nil, fmt.Errorf("no SVG element found in rendered output") + } + return []byte(svgXML), nil +} + +// clipRect represents a bounding rectangle. +type clipRect struct { + X float64 `json:"x"` + Y float64 `json:"y"` + Width float64 `json:"width"` + Height float64 `json:"height"` +} + +// getSVGBounds gets the bounding rect of the SVG element. +func getSVGBounds(ctx context.Context) (*clipRect, error) { + var boundsJSON string + err := chromedp.Run(ctx, + chromedp.Evaluate(`(() => { + const svg = document.querySelector('#container svg'); + if (!svg) return JSON.stringify({x:0, y:0, width:800, height:600}); + const rect = svg.getBoundingClientRect(); + return JSON.stringify({ + x: Math.floor(rect.left), + y: Math.floor(rect.top), + width: Math.ceil(rect.width), + height: Math.ceil(rect.height) + }); + })()`, &boundsJSON), + ) + if err != nil { + return nil, fmt.Errorf("failed to get SVG bounds: %w", err) + } + + var bounds clipRect + if err := json.Unmarshal([]byte(boundsJSON), &bounds); err != nil { + return nil, fmt.Errorf("failed to parse SVG bounds: %w", err) + } + return &bounds, nil +} + +// capturePNG captures a PNG screenshot clipped to the SVG bounds. +func capturePNG(ctx context.Context, opts RenderOpts) ([]byte, error) { + bounds, err := getSVGBounds(ctx) + if err != nil { + return nil, err + } + + // Resize viewport to fit the SVG + newWidth := int64(bounds.X + bounds.Width) + newHeight := int64(bounds.Y + bounds.Height) + if err := chromedp.Run(ctx, + emulation.SetDeviceMetricsOverride(newWidth, newHeight, float64(opts.Scale), false), + ); err != nil { + return nil, fmt.Errorf("failed to resize viewport for PNG: %w", err) + } + + // Small delay to let the resize settle + time.Sleep(100 * time.Millisecond) + + clip := &page.Viewport{ + X: bounds.X, + Y: bounds.Y, + Width: bounds.Width, + Height: bounds.Height, + Scale: 1, + } + + // Set transparent background if requested + if opts.BackgroundColor == "transparent" { + if err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { + return emulation.SetDefaultBackgroundColorOverride().WithColor(&cdp.RGBA{R: 0, G: 0, B: 0, A: 0}).Do(ctx) + })); err != nil { + return nil, fmt.Errorf("failed to set transparent background: %w", err) + } + } + + var buf []byte + captureParams := page.CaptureScreenshot(). + WithFormat(page.CaptureScreenshotFormatPng). + WithClip(clip). + WithCaptureBeyondViewport(true) + + if err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { + var err error + buf, err = captureParams.Do(ctx) + return err + })); err != nil { + return nil, fmt.Errorf("failed to capture PNG: %w", err) + } + + // Reset background color override + if opts.BackgroundColor == "transparent" { + _ = chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { + return emulation.SetDefaultBackgroundColorOverride().Do(ctx) + })) + } + + return buf, nil +} + +// capturePDF captures a PDF of the page. +func capturePDF(ctx context.Context, opts RenderOpts) ([]byte, error) { + // Set transparent background if requested + if opts.BackgroundColor == "transparent" { + if err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { + return emulation.SetDefaultBackgroundColorOverride().WithColor(&cdp.RGBA{R: 0, G: 0, B: 0, A: 0}).Do(ctx) + })); err != nil { + return nil, fmt.Errorf("failed to set transparent background: %w", err) + } + } + + printParams := page.PrintToPDF() + + if opts.PdfFit { + bounds, err := getSVGBounds(ctx) + if err != nil { + return nil, err + } + + // Convert px to inches (96 DPI) + widthInches := (math.Ceil(bounds.Width) + bounds.X*2) / 96.0 + heightInches := (math.Ceil(bounds.Height) + bounds.Y*2) / 96.0 + + printParams = printParams. + WithPaperWidth(widthInches). + WithPaperHeight(heightInches). + WithMarginTop(0). + WithMarginBottom(0). + WithMarginLeft(0). + WithMarginRight(0). + WithPageRanges("1-1") + } + + printParams = printParams.WithPrintBackground(true) + + var buf []byte + if err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { + var err error + buf, _, err = printParams.Do(ctx) + return err + })); err != nil { + return nil, fmt.Errorf("failed to generate PDF: %w", err) + } + + // Reset background color override + if opts.BackgroundColor == "transparent" { + _ = chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { + return emulation.SetDefaultBackgroundColorOverride().Do(ctx) + })) + } + + return buf, nil +} diff --git a/internal/renderer/template.go b/internal/renderer/template.go new file mode 100644 index 0000000..383fa5c --- /dev/null +++ b/internal/renderer/template.go @@ -0,0 +1,133 @@ +package renderer + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/coolamit/mermaid-cli/internal/config" + "github.com/coolamit/mermaid-cli/internal/icons" + "github.com/coolamit/mermaid-cli/web" +) + +// RenderOpts contains all options needed to render a mermaid diagram. +type RenderOpts struct { + MermaidConfig config.MermaidConfig + BackgroundColor string + CSS string + SVGId string + Width int + Height int + Scale int + PdfFit bool + SvgFit bool + IconPacks []icons.IconPack +} + +// BuildPageHTML constructs the full HTML page with embedded mermaid.js, config, and diagram. +func BuildPageHTML(definition string, opts RenderOpts) (string, error) { + mermaidConfigJSON, err := opts.MermaidConfig.ToJSON() + if err != nil { + return "", fmt.Errorf("failed to serialize mermaid config: %w", err) + } + + definitionJSON, err := json.Marshal(definition) + if err != nil { + return "", fmt.Errorf("failed to serialize diagram definition: %w", err) + } + + svgIdJSON, err := json.Marshal(opts.SVGId) + if err != nil { + return "", fmt.Errorf("failed to serialize svgId: %w", err) + } + + bgColorJSON, err := json.Marshal(opts.BackgroundColor) + if err != nil { + return "", fmt.Errorf("failed to serialize backgroundColor: %w", err) + } + + cssJSON, err := json.Marshal(opts.CSS) + if err != nil { + return "", fmt.Errorf("failed to serialize CSS: %w", err) + } + + iconPackJS := icons.GenerateIconPackJS(opts.IconPacks) + + // Build the full HTML page + var sb strings.Builder + sb.WriteString(` + + + + + +
+ + + + +`, mermaidConfigJSON, string(definitionJSON), string(svgIdJSON), string(bgColorJSON), string(cssJSON))) + + return sb.String(), nil +} diff --git a/internal/renderer/template_test.go b/internal/renderer/template_test.go new file mode 100644 index 0000000..6959a2f --- /dev/null +++ b/internal/renderer/template_test.go @@ -0,0 +1,102 @@ +package renderer + +import ( + "strings" + "testing" + + "github.com/coolamit/mermaid-cli/internal/config" + "github.com/coolamit/mermaid-cli/internal/icons" +) + +func defaultOpts() RenderOpts { + return RenderOpts{ + MermaidConfig: config.MermaidConfig{"theme": "default"}, + BackgroundColor: "white", + } +} + +func TestBuildPageHTML_Basic(t *testing.T) { + html, err := BuildPageHTML("graph TD;\n A-->B;", defaultOpts()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + checks := []struct { + label string + contains string + }{ + {"DOCTYPE", ""}, + {"container div", `id="container"`}, + // json.Marshal escapes > to \u003e, so A-->B becomes A--\u003eB + {"diagram definition", `A--\u003eB`}, + {"mermaid config", `"theme":"default"`}, + } + for _, c := range checks { + if !strings.Contains(html, c.contains) { + t.Errorf("expected HTML to contain %s (%q)", c.label, c.contains) + } + } +} + +func TestBuildPageHTML_WithCSS(t *testing.T) { + opts := defaultOpts() + opts.CSS = "svg { border: 1px solid red; }" + + html, err := BuildPageHTML("graph TD; A-->B;", opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(html, "border: 1px solid red") { + t.Error("expected custom CSS in output") + } +} + +func TestBuildPageHTML_WithIconPacks(t *testing.T) { + opts := defaultOpts() + opts.IconPacks = []icons.IconPack{ + {Name: "logos", URL: "https://example.com/logos.json"}, + } + + html, err := BuildPageHTML("graph TD; A-->B;", opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(html, "mermaid.registerIconPacks") { + t.Error("expected registerIconPacks in output") + } + if !strings.Contains(html, "logos") { + t.Error("expected icon pack name in output") + } +} + +func TestBuildPageHTML_WithSVGId(t *testing.T) { + opts := defaultOpts() + opts.SVGId = "custom-svg-id" + + html, err := BuildPageHTML("graph TD; A-->B;", opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(html, "custom-svg-id") { + t.Error("expected custom SVG ID in output") + } +} + +func TestBuildPageHTML_SpecialChars(t *testing.T) { + // Definition contains literal quotes and a backslash + definition := "graph TD; A[\"Node with quotes and \\\\backslash\"]-->B;" + + html, err := BuildPageHTML(definition, defaultOpts()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // json.Marshal escapes " to \" and \ to \\ + // In the HTML: \" before "Node" (raw string `\"` matches backslash + quote) + if !strings.Contains(html, `\"Node with quotes`) { + t.Errorf("expected JSON-escaped quotes in output") + } + // Two input backslashes become four in JSON (each \ → \\) + if !strings.Contains(html, `\\\\backslash`) { + t.Errorf("expected JSON-escaped backslash in output") + } +} diff --git a/version b/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.1.0 diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..7d156e6 --- /dev/null +++ b/web/embed.go @@ -0,0 +1,12 @@ +package web + +import _ "embed" + +//go:embed template.html +var TemplateHTML string + +//go:embed mermaid.min.js +var MermaidJS []byte + +//go:embed mermaid-zenuml.js +var MermaidZenUMLJS []byte diff --git a/web/mermaid-zenuml.js b/web/mermaid-zenuml.js new file mode 100644 index 0000000..d181ee3 --- /dev/null +++ b/web/mermaid-zenuml.js @@ -0,0 +1,84029 @@ +"use strict"; +var __esbuild_esm_mermaid_nm; +(__esbuild_esm_mermaid_nm ||= {})["mermaid-zenuml"] = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); + var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; + }; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to2, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to2, key) && key !== except) + __defProp(to2, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to2; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + + // src/mermaidUtils.ts + var warning, log, setLogLevel, getConfig, sanitizeText, setupGraphViewbox, injectUtils; + var init_mermaidUtils = __esm({ + "src/mermaidUtils.ts"() { + "use strict"; + warning = /* @__PURE__ */ __name((s) => { + console.error("Log function was called before initialization", s); + }, "warning"); + log = { + trace: warning, + debug: warning, + info: warning, + warn: warning, + error: warning, + fatal: warning + }; + injectUtils = /* @__PURE__ */ __name((_log, _setLogLevel, _getConfig, _sanitizeText, _setupGraphViewbox) => { + _log.info("Mermaid utils injected"); + log.trace = _log.trace; + log.debug = _log.debug; + log.info = _log.info; + log.warn = _log.warn; + log.error = _log.error; + log.fatal = _log.fatal; + setLogLevel = _setLogLevel; + getConfig = _getConfig; + sanitizeText = _sanitizeText; + setupGraphViewbox = _setupGraphViewbox; + }, "injectUtils"); + } + }); + + // src/parser.ts + var parser_default; + var init_parser = __esm({ + "src/parser.ts"() { + "use strict"; + parser_default = { + parse: /* @__PURE__ */ __name(() => { + }, "parse") + }; + } + }); + + // ../../node_modules/.pnpm/@zenuml+core@3.35.2/node_modules/@zenuml/core/dist/zenuml.esm.mjs + function hb(i, e) { + for (var t = 0; t < e.length; t++) { + const n = e[t]; + if (typeof n != "string" && !Array.isArray(n)) { + for (const s in n) + if (s !== "default" && !(s in i)) { + const l = Object.getOwnPropertyDescriptor(n, s); + l && Object.defineProperty(i, s, l.get ? l : { + enumerable: true, + get: /* @__PURE__ */ __name(() => n[s], "get") + }); + } + } + } + return Object.freeze(Object.defineProperty(i, Symbol.toStringTag, { value: "Module" })); + } + function K4(i) { + return i && i.__esModule && Object.prototype.hasOwnProperty.call(i, "default") ? i.default : i; + } + function fb() { + if (l_) return T5; + l_ = 1; + var i = Symbol.for("react.transitional.element"), e = Symbol.for("react.fragment"); + function t(n, s, l) { + var c = null; + if (l !== void 0 && (c = "" + l), s.key !== void 0 && (c = "" + s.key), "key" in s) { + l = {}; + for (var L in s) + L !== "key" && (l[L] = s[L]); + } else l = s; + return s = l.ref, { + $$typeof: i, + type: n, + key: c, + ref: s !== void 0 ? s : null, + props: l + }; + } + __name(t, "t"); + return T5.Fragment = e, T5.jsx = t, T5.jsxs = t, T5; + } + function Eb() { + return c_ || (c_ = 1, Wi.exports = fb()), Wi.exports; + } + function Sb() { + if (u_) return $i; + u_ = 1; + function i(t) { + try { + return JSON.stringify(t); + } catch { + return '"[Circular]"'; + } + } + __name(i, "i"); + $i = e; + function e(t, n, s) { + var l = s && s.stringify || i, c = 1; + if (typeof t == "object" && t !== null) { + var L = n.length + c; + if (L === 1) return t; + var d = new Array(L); + d[0] = l(t); + for (var _ = 1; _ < L; _++) + d[_] = l(n[_]); + return d.join(" "); + } + if (typeof t != "string") + return t; + var p = n.length; + if (p === 0) return t; + for (var m = "", f = 1 - c, h = -1, R = t && t.length || 0, b = 0; b < R; ) { + if (t.charCodeAt(b) === 37 && b + 1 < R) { + switch (h = h > -1 ? h : 0, t.charCodeAt(b + 1)) { + case 100: + // 'd' + case 102: + if (f >= p || n[f] == null) break; + h < b && (m += t.slice(h, b)), m += Number(n[f]), h = b + 2, b++; + break; + case 105: + if (f >= p || n[f] == null) break; + h < b && (m += t.slice(h, b)), m += Math.floor(Number(n[f])), h = b + 2, b++; + break; + case 79: + // 'O' + case 111: + // 'o' + case 106: + if (f >= p || n[f] === void 0) break; + h < b && (m += t.slice(h, b)); + var M = typeof n[f]; + if (M === "string") { + m += "'" + n[f] + "'", h = b + 2, b++; + break; + } + if (M === "function") { + m += n[f].name || "", h = b + 2, b++; + break; + } + m += l(n[f]), h = b + 2, b++; + break; + case 115: + if (f >= p) + break; + h < b && (m += t.slice(h, b)), m += String(n[f]), h = b + 2, b++; + break; + case 37: + h < b && (m += t.slice(h, b)), m += "%", h = b + 2, b++, f--; + break; + } + ++f; + } + ++b; + } + return h === -1 ? t : (h < R && (m += t.slice(h)), m); + } + __name(e, "e"); + return $i; + } + function Tb() { + if (L_) return v5.exports; + L_ = 1; + const i = Sb(); + v5.exports = p; + const e = R2().console || {}, t = { + mapHttpRequest: k, + mapHttpResponse: k, + wrapRequestSerializer: n2, + wrapResponseSerializer: n2, + wrapErrorSerializer: n2, + req: k, + res: k, + err: F, + errWithCause: F + }; + function n(H, A) { + return H === "silent" ? 1 / 0 : A.levels.values[H]; + } + __name(n, "n"); + const s = Symbol("pino.logFuncs"), l = Symbol("pino.hierarchy"), c = { + error: "log", + fatal: "error", + warn: "error", + info: "log", + debug: "log", + trace: "log" + }; + function L(H, A) { + const B = { + logger: A, + parent: H[l] + }; + A[l] = B; + } + __name(L, "L"); + function d(H, A, B) { + const e2 = {}; + A.forEach((u2) => { + e2[u2] = B[u2] ? B[u2] : e[u2] || e[c[u2] || "log"] || K; + }), H[s] = e2; + } + __name(d, "d"); + function _(H, A) { + return Array.isArray(H) ? H.filter(function(e2) { + return e2 !== "!stdSerializers.err"; + }) : H === true ? Object.keys(A) : false; + } + __name(_, "_"); + function p(H) { + H = H || {}, H.browser = H.browser || {}; + const A = H.browser.transmit; + if (A && typeof A.send != "function") + throw Error("pino: transmit option must have a send function"); + const B = H.browser.write || e; + H.browser.write && (H.browser.asObject = true); + const e2 = H.serializers || {}, u2 = _(H.browser.serialize, e2); + let y = H.browser.serialize; + Array.isArray(H.browser.serialize) && H.browser.serialize.indexOf("!stdSerializers.err") > -1 && (y = false); + const q = Object.keys(H.customLevels || {}), s2 = ["error", "fatal", "warn", "info", "debug", "trace"].concat(q); + typeof B == "function" && s2.forEach(function(p1) { + B[p1] = B; + }), (H.enabled === false || H.browser.disabled) && (H.level = "silent"); + const i2 = H.level || "info", a2 = Object.create(B); + a2.log || (a2.log = K), d(a2, s2, B), L({}, a2), Object.defineProperty(a2, "levelVal", { + get: f2 + }), Object.defineProperty(a2, "level", { + get: V2, + set: q2 + }); + const _2 = { + transmit: A, + serialize: u2, + asObject: H.browser.asObject, + formatters: H.browser.formatters, + levels: s2, + timestamp: U(H) + }; + a2.levels = m(H), a2.level = i2, a2.setMaxListeners = a2.getMaxListeners = a2.emit = a2.addListener = a2.on = a2.prependListener = a2.once = a2.prependOnceListener = a2.removeListener = a2.removeAllListeners = a2.listeners = a2.listenerCount = a2.eventNames = a2.write = a2.flush = K, a2.serializers = e2, a2._serialize = u2, a2._stdErrSerialize = y, a2.child = v1, A && (a2._logEvent = N()); + function f2() { + return n(this.level, this); + } + __name(f2, "f2"); + function V2() { + return this._level; + } + __name(V2, "V2"); + function q2(p1) { + if (p1 !== "silent" && !this.levels.values[p1]) + throw Error("unknown level " + p1); + this._level = p1, R(this, _2, a2, "error"), R(this, _2, a2, "fatal"), R(this, _2, a2, "warn"), R(this, _2, a2, "info"), R(this, _2, a2, "debug"), R(this, _2, a2, "trace"), q.forEach((P1) => { + R(this, _2, a2, P1); + }); + } + __name(q2, "q2"); + function v1(p1, P1) { + if (!p1) + throw new Error("missing bindings for child Pino"); + P1 = P1 || {}, u2 && p1.serializers && (P1.serializers = p1.serializers); + const z1 = P1.serializers; + if (u2 && z1) { + var he = Object.assign({}, e2, z1), de = H.browser.serialize === true ? Object.keys(he) : u2; + delete p1.serializers, O([p1], de, he, this._stdErrSerialize); + } + function g3(O1) { + this._childLevel = (O1._childLevel | 0) + 1, this.bindings = p1, he && (this.serializers = he, this._serialize = de), A && (this._logEvent = N( + [].concat(O1._logEvent.bindings, p1) + )); + } + __name(g3, "g3"); + g3.prototype = this; + const we = new g3(this); + return L(this, we), we.level = this.level, we; + } + __name(v1, "v1"); + return a2; + } + __name(p, "p"); + function m(H) { + const A = H.customLevels || {}, B = Object.assign({}, p.levels.values, A), e2 = Object.assign({}, p.levels.labels, f(A)); + return { + values: B, + labels: e2 + }; + } + __name(m, "m"); + function f(H) { + const A = {}; + return Object.keys(H).forEach(function(B) { + A[H[B]] = B; + }), A; + } + __name(f, "f"); + p.levels = { + values: { + fatal: 60, + error: 50, + warn: 40, + info: 30, + debug: 20, + trace: 10 + }, + labels: { + 10: "trace", + 20: "debug", + 30: "info", + 40: "warn", + 50: "error", + 60: "fatal" + } + }, p.stdSerializers = t, p.stdTimeFunctions = Object.assign({}, { nullTime: X, epochTime: r2, unixTime: t2, isoTime: g2 }); + function h(H) { + const A = []; + H.bindings && A.push(H.bindings); + let B = H[l]; + for (; B.parent; ) + B = B.parent, B.logger.bindings && A.push(B.logger.bindings); + return A.reverse(); + } + __name(h, "h"); + function R(H, A, B, e2) { + if (Object.defineProperty(H, e2, { + value: n(H.level, B) > n(e2, B) ? K : B[s][e2], + writable: true, + enumerable: true, + configurable: true + }), !A.transmit && H[e2] === K) + return; + H[e2] = M(H, A, B, e2); + const u2 = h(H); + u2.length !== 0 && (H[e2] = b(u2, H[e2])); + } + __name(R, "R"); + function b(H, A) { + return function() { + return A.apply(this, [...H, ...arguments]); + }; + } + __name(b, "b"); + function M(H, A, B, e2) { + return /* @__PURE__ */ function(u2) { + return function() { + const q = A.timestamp(), s2 = new Array(arguments.length), i2 = Object.getPrototypeOf && Object.getPrototypeOf(this) === e ? e : this; + for (var a2 = 0; a2 < s2.length; a2++) s2[a2] = arguments[a2]; + if (A.serialize && !A.asObject && O(s2, this._serialize, this.serializers, this._stdErrSerialize), A.asObject || A.formatters ? u2.call(i2, w(this, e2, s2, q, A.formatters)) : u2.apply(i2, s2), A.transmit) { + const _2 = A.transmit.level || H._level, f2 = B.levels.values[_2], V2 = B.levels.values[e2]; + if (V2 < f2) return; + D(this, { + ts: q, + methodLevel: e2, + methodValue: V2, + transmitValue: B.levels.values[A.transmit.level || H._level], + send: A.transmit.send, + val: n(H._level, B) + }, s2); + } + }; + }(H[s][e2]); + } + __name(M, "M"); + function w(H, A, B, e2, u2 = {}) { + const { + level: y = /* @__PURE__ */ __name(() => H.levels.values[A], "y"), + log: q = /* @__PURE__ */ __name((V2) => V2, "q") + } = u2; + H._serialize && O(B, H._serialize, H.serializers, H._stdErrSerialize); + const s2 = B.slice(); + let i2 = s2[0]; + const a2 = {}; + e2 && (a2.time = e2), a2.level = y(A, H.levels.values[A]); + let _2 = (H._childLevel | 0) + 1; + if (_2 < 1 && (_2 = 1), i2 !== null && typeof i2 == "object") { + for (; _2-- && typeof s2[0] == "object"; ) + Object.assign(a2, s2.shift()); + i2 = s2.length ? i(s2.shift(), s2) : void 0; + } else typeof i2 == "string" && (i2 = i(s2.shift(), s2)); + return i2 !== void 0 && (a2.msg = i2), q(a2); + } + __name(w, "w"); + function O(H, A, B, e2) { + for (const u2 in H) + if (e2 && H[u2] instanceof Error) + H[u2] = p.stdSerializers.err(H[u2]); + else if (typeof H[u2] == "object" && !Array.isArray(H[u2])) + for (const y in H[u2]) + A && A.indexOf(y) > -1 && y in B && (H[u2][y] = B[y](H[u2][y])); + } + __name(O, "O"); + function D(H, A, B) { + const e2 = A.send, u2 = A.ts, y = A.methodLevel, q = A.methodValue, s2 = A.val, i2 = H._logEvent.bindings; + O( + B, + H._serialize || Object.keys(H.serializers), + H.serializers, + H._stdErrSerialize === void 0 ? true : H._stdErrSerialize + ), H._logEvent.ts = u2, H._logEvent.messages = B.filter(function(a2) { + return i2.indexOf(a2) === -1; + }), H._logEvent.level.label = y, H._logEvent.level.value = q, e2(y, H._logEvent, s2), H._logEvent = N(i2); + } + __name(D, "D"); + function N(H) { + return { + ts: 0, + messages: [], + bindings: H || [], + level: { label: "", value: 0 } + }; + } + __name(N, "N"); + function F(H) { + const A = { + type: H.constructor.name, + msg: H.message, + stack: H.stack + }; + for (const B in H) + A[B] === void 0 && (A[B] = H[B]); + return A; + } + __name(F, "F"); + function U(H) { + return typeof H.timestamp == "function" ? H.timestamp : H.timestamp === false ? X : r2; + } + __name(U, "U"); + function k() { + return {}; + } + __name(k, "k"); + function n2(H) { + return H; + } + __name(n2, "n2"); + function K() { + } + __name(K, "K"); + function X() { + return false; + } + __name(X, "X"); + function r2() { + return Date.now(); + } + __name(r2, "r2"); + function t2() { + return Math.round(Date.now() / 1e3); + } + __name(t2, "t2"); + function g2() { + return new Date(Date.now()).toISOString(); + } + __name(g2, "g2"); + function R2() { + function H(A) { + return typeof A < "u" && A; + } + __name(H, "H"); + try { + return typeof globalThis < "u" || Object.defineProperty(Object.prototype, "globalThis", { + get: /* @__PURE__ */ __name(function() { + return delete Object.prototype.globalThis, this.globalThis = this; + }, "get"), + configurable: true + }), globalThis; + } catch { + return H(self) || H(window) || H(this) || {}; + } + } + __name(R2, "R2"); + return v5.exports.default = p, v5.exports.pino = p, v5.exports; + } + function xb(i, e) { + i[e] = (console[e] || console.log).bind(console); + } + function wb(i, e, t) { + i[e] = (console[e] || console.log).bind( + console, + t[0], + t[1] + ); + } + function Mb(i) { + d_.forEach((t) => xb(i, t)); + const e = i.child; + return i.child = function(t) { + const n = e.call(i, t); + return d_.forEach( + (s) => wb(n, s, ["%c" + t.name || "", "color: #00f"]) + ), n; + }, i; + } + function J1(i, e) { + const t = `atom${++kb}`, n = { + toString() { + return (oc ? "production" : void 0) !== "production" && this.debugLabel ? t + ":" + this.debugLabel : t; + } + }; + return typeof i == "function" ? n.read = i : (n.init = i, n.read = Pb, n.write = Fb), e && (n.write = e), n; + } + function Pb(i) { + return i(this); + } + function Fb(i, e, t) { + return e( + this, + typeof t == "function" ? t(i(this)) : t + ); + } + function lc() { + return (oc ? "production" : void 0) !== "production" ? Ub() : eE(); + } + function Zb() { + return R5 || (R5 = lc(), (oc ? "production" : void 0) !== "production" && (globalThis.__JOTAI_DEFAULT_STORE__ || (globalThis.__JOTAI_DEFAULT_STORE__ = R5), globalThis.__JOTAI_DEFAULT_STORE__ !== R5 && console.warn( + "Detected multiple Jotai instances. It may cause unexpected behavior with the default store. https://github.com/pmndrs/jotai/discussions/2044" + ))), R5; + } + function Bb() { + if (f_) return J2; + f_ = 1; + var i = Symbol.for("react.transitional.element"), e = Symbol.for("react.portal"), t = Symbol.for("react.fragment"), n = Symbol.for("react.strict_mode"), s = Symbol.for("react.profiler"), l = Symbol.for("react.consumer"), c = Symbol.for("react.context"), L = Symbol.for("react.forward_ref"), d = Symbol.for("react.suspense"), _ = Symbol.for("react.memo"), p = Symbol.for("react.lazy"), m = Symbol.iterator; + function f(y) { + return y === null || typeof y != "object" ? null : (y = m && y[m] || y["@@iterator"], typeof y == "function" ? y : null); + } + __name(f, "f"); + var h = { + isMounted: /* @__PURE__ */ __name(function() { + return false; + }, "isMounted"), + enqueueForceUpdate: /* @__PURE__ */ __name(function() { + }, "enqueueForceUpdate"), + enqueueReplaceState: /* @__PURE__ */ __name(function() { + }, "enqueueReplaceState"), + enqueueSetState: /* @__PURE__ */ __name(function() { + }, "enqueueSetState") + }, R = Object.assign, b = {}; + function M(y, q, s2) { + this.props = y, this.context = q, this.refs = b, this.updater = s2 || h; + } + __name(M, "M"); + M.prototype.isReactComponent = {}, M.prototype.setState = function(y, q) { + if (typeof y != "object" && typeof y != "function" && y != null) + throw Error( + "takes an object of state variables to update or a function which returns an object of state variables." + ); + this.updater.enqueueSetState(this, y, q, "setState"); + }, M.prototype.forceUpdate = function(y) { + this.updater.enqueueForceUpdate(this, y, "forceUpdate"); + }; + function w() { + } + __name(w, "w"); + w.prototype = M.prototype; + function O(y, q, s2) { + this.props = y, this.context = q, this.refs = b, this.updater = s2 || h; + } + __name(O, "O"); + var D = O.prototype = new w(); + D.constructor = O, R(D, M.prototype), D.isPureReactComponent = true; + var N = Array.isArray, F = { H: null, A: null, T: null, S: null, V: null }, U = Object.prototype.hasOwnProperty; + function k(y, q, s2, i2, a2, _2) { + return s2 = _2.ref, { + $$typeof: i, + type: y, + key: q, + ref: s2 !== void 0 ? s2 : null, + props: _2 + }; + } + __name(k, "k"); + function n2(y, q) { + return k( + y.type, + q, + void 0, + void 0, + void 0, + y.props + ); + } + __name(n2, "n2"); + function K(y) { + return typeof y == "object" && y !== null && y.$$typeof === i; + } + __name(K, "K"); + function X(y) { + var q = { "=": "=0", ":": "=2" }; + return "$" + y.replace(/[=:]/g, function(s2) { + return q[s2]; + }); + } + __name(X, "X"); + var r2 = /\/+/g; + function t2(y, q) { + return typeof y == "object" && y !== null && y.key != null ? X("" + y.key) : q.toString(36); + } + __name(t2, "t2"); + function g2() { + } + __name(g2, "g2"); + function R2(y) { + switch (y.status) { + case "fulfilled": + return y.value; + case "rejected": + throw y.reason; + default: + switch (typeof y.status == "string" ? y.then(g2, g2) : (y.status = "pending", y.then( + function(q) { + y.status === "pending" && (y.status = "fulfilled", y.value = q); + }, + function(q) { + y.status === "pending" && (y.status = "rejected", y.reason = q); + } + )), y.status) { + case "fulfilled": + return y.value; + case "rejected": + throw y.reason; + } + } + throw y; + } + __name(R2, "R2"); + function H(y, q, s2, i2, a2) { + var _2 = typeof y; + (_2 === "undefined" || _2 === "boolean") && (y = null); + var f2 = false; + if (y === null) f2 = true; + else + switch (_2) { + case "bigint": + case "string": + case "number": + f2 = true; + break; + case "object": + switch (y.$$typeof) { + case i: + case e: + f2 = true; + break; + case p: + return f2 = y._init, H( + f2(y._payload), + q, + s2, + i2, + a2 + ); + } + } + if (f2) + return a2 = a2(y), f2 = i2 === "" ? "." + t2(y, 0) : i2, N(a2) ? (s2 = "", f2 != null && (s2 = f2.replace(r2, "$&/") + "/"), H(a2, q, s2, "", function(v1) { + return v1; + })) : a2 != null && (K(a2) && (a2 = n2( + a2, + s2 + (a2.key == null || y && y.key === a2.key ? "" : ("" + a2.key).replace( + r2, + "$&/" + ) + "/") + f2 + )), q.push(a2)), 1; + f2 = 0; + var V2 = i2 === "" ? "." : i2 + ":"; + if (N(y)) + for (var q2 = 0; q2 < y.length; q2++) + i2 = y[q2], _2 = V2 + t2(i2, q2), f2 += H( + i2, + q, + s2, + _2, + a2 + ); + else if (q2 = f(y), typeof q2 == "function") + for (y = q2.call(y), q2 = 0; !(i2 = y.next()).done; ) + i2 = i2.value, _2 = V2 + t2(i2, q2++), f2 += H( + i2, + q, + s2, + _2, + a2 + ); + else if (_2 === "object") { + if (typeof y.then == "function") + return H( + R2(y), + q, + s2, + i2, + a2 + ); + throw q = String(y), Error( + "Objects are not valid as a React child (found: " + (q === "[object Object]" ? "object with keys {" + Object.keys(y).join(", ") + "}" : q) + "). If you meant to render a collection of children, use an array instead." + ); + } + return f2; + } + __name(H, "H"); + function A(y, q, s2) { + if (y == null) return y; + var i2 = [], a2 = 0; + return H(y, i2, "", "", function(_2) { + return q.call(s2, _2, a2++); + }), i2; + } + __name(A, "A"); + function B(y) { + if (y._status === -1) { + var q = y._result; + q = q(), q.then( + function(s2) { + (y._status === 0 || y._status === -1) && (y._status = 1, y._result = s2); + }, + function(s2) { + (y._status === 0 || y._status === -1) && (y._status = 2, y._result = s2); + } + ), y._status === -1 && (y._status = 0, y._result = q); + } + if (y._status === 1) return y._result.default; + throw y._result; + } + __name(B, "B"); + var e2 = typeof reportError == "function" ? reportError : function(y) { + if (typeof window == "object" && typeof window.ErrorEvent == "function") { + var q = new window.ErrorEvent("error", { + bubbles: true, + cancelable: true, + message: typeof y == "object" && y !== null && typeof y.message == "string" ? String(y.message) : String(y), + error: y + }); + if (!window.dispatchEvent(q)) return; + } else if (typeof process == "object" && typeof process.emit == "function") { + process.emit("uncaughtException", y); + return; + } + console.error(y); + }; + function u2() { + } + __name(u2, "u2"); + return J2.Children = { + map: A, + forEach: /* @__PURE__ */ __name(function(y, q, s2) { + A( + y, + function() { + q.apply(this, arguments); + }, + s2 + ); + }, "forEach"), + count: /* @__PURE__ */ __name(function(y) { + var q = 0; + return A(y, function() { + q++; + }), q; + }, "count"), + toArray: /* @__PURE__ */ __name(function(y) { + return A(y, function(q) { + return q; + }) || []; + }, "toArray"), + only: /* @__PURE__ */ __name(function(y) { + if (!K(y)) + throw Error( + "React.Children.only expected to receive a single React element child." + ); + return y; + }, "only") + }, J2.Component = M, J2.Fragment = t, J2.Profiler = s, J2.PureComponent = O, J2.StrictMode = n, J2.Suspense = d, J2.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = F, J2.__COMPILER_RUNTIME = { + __proto__: null, + c: /* @__PURE__ */ __name(function(y) { + return F.H.useMemoCache(y); + }, "c") + }, J2.cache = function(y) { + return function() { + return y.apply(null, arguments); + }; + }, J2.cloneElement = function(y, q, s2) { + if (y == null) + throw Error( + "The argument must be a React element, but you passed " + y + "." + ); + var i2 = R({}, y.props), a2 = y.key, _2 = void 0; + if (q != null) + for (f2 in q.ref !== void 0 && (_2 = void 0), q.key !== void 0 && (a2 = "" + q.key), q) + !U.call(q, f2) || f2 === "key" || f2 === "__self" || f2 === "__source" || f2 === "ref" && q.ref === void 0 || (i2[f2] = q[f2]); + var f2 = arguments.length - 2; + if (f2 === 1) i2.children = s2; + else if (1 < f2) { + for (var V2 = Array(f2), q2 = 0; q2 < f2; q2++) + V2[q2] = arguments[q2 + 2]; + i2.children = V2; + } + return k(y.type, a2, void 0, void 0, _2, i2); + }, J2.createContext = function(y) { + return y = { + $$typeof: c, + _currentValue: y, + _currentValue2: y, + _threadCount: 0, + Provider: null, + Consumer: null + }, y.Provider = y, y.Consumer = { + $$typeof: l, + _context: y + }, y; + }, J2.createElement = function(y, q, s2) { + var i2, a2 = {}, _2 = null; + if (q != null) + for (i2 in q.key !== void 0 && (_2 = "" + q.key), q) + U.call(q, i2) && i2 !== "key" && i2 !== "__self" && i2 !== "__source" && (a2[i2] = q[i2]); + var f2 = arguments.length - 2; + if (f2 === 1) a2.children = s2; + else if (1 < f2) { + for (var V2 = Array(f2), q2 = 0; q2 < f2; q2++) + V2[q2] = arguments[q2 + 2]; + a2.children = V2; + } + if (y && y.defaultProps) + for (i2 in f2 = y.defaultProps, f2) + a2[i2] === void 0 && (a2[i2] = f2[i2]); + return k(y, _2, void 0, void 0, null, a2); + }, J2.createRef = function() { + return { current: null }; + }, J2.forwardRef = function(y) { + return { $$typeof: L, render: y }; + }, J2.isValidElement = K, J2.lazy = function(y) { + return { + $$typeof: p, + _payload: { _status: -1, _result: y }, + _init: B + }; + }, J2.memo = function(y, q) { + return { + $$typeof: _, + type: y, + compare: q === void 0 ? null : q + }; + }, J2.startTransition = function(y) { + var q = F.T, s2 = {}; + F.T = s2; + try { + var i2 = y(), a2 = F.S; + a2 !== null && a2(s2, i2), typeof i2 == "object" && i2 !== null && typeof i2.then == "function" && i2.then(u2, e2); + } catch (_2) { + e2(_2); + } finally { + F.T = q; + } + }, J2.unstable_useCacheRefresh = function() { + return F.H.useCacheRefresh(); + }, J2.use = function(y) { + return F.H.use(y); + }, J2.useActionState = function(y, q, s2) { + return F.H.useActionState(y, q, s2); + }, J2.useCallback = function(y, q) { + return F.H.useCallback(y, q); + }, J2.useContext = function(y) { + return F.H.useContext(y); + }, J2.useDebugValue = function() { + }, J2.useDeferredValue = function(y, q) { + return F.H.useDeferredValue(y, q); + }, J2.useEffect = function(y, q, s2) { + var i2 = F.H; + if (typeof s2 == "function") + throw Error( + "useEffect CRUD overload is not enabled in this build of React." + ); + return i2.useEffect(y, q); + }, J2.useId = function() { + return F.H.useId(); + }, J2.useImperativeHandle = function(y, q, s2) { + return F.H.useImperativeHandle(y, q, s2); + }, J2.useInsertionEffect = function(y, q) { + return F.H.useInsertionEffect(y, q); + }, J2.useLayoutEffect = function(y, q) { + return F.H.useLayoutEffect(y, q); + }, J2.useMemo = function(y, q) { + return F.H.useMemo(y, q); + }, J2.useOptimistic = function(y, q) { + return F.H.useOptimistic(y, q); + }, J2.useReducer = function(y, q, s2) { + return F.H.useReducer(y, q, s2); + }, J2.useRef = function(y) { + return F.H.useRef(y); + }, J2.useState = function(y) { + return F.H.useState(y); + }, J2.useSyncExternalStore = function(y, q, s2) { + return F.H.useSyncExternalStore( + y, + q, + s2 + ); + }, J2.useTransition = function() { + return F.H.useTransition(); + }, J2.version = "19.1.0", J2; + } + function cc() { + return E_ || (E_ = 1, Xi.exports = Bb()), Xi.exports; + } + function uc(i) { + return v.useContext(tE) || Zb(); + } + function zb({ + children: i, + store: e + }) { + const t = v.useRef(void 0); + return !e && !t.current && (t.current = lc()), v.createElement( + tE.Provider, + { + value: e || t.current + }, + i + ); + } + function j2(i, e) { + const t = uc(), [[n, s, l], c] = v.useReducer( + (_) => { + const p = t.get(i); + return Object.is(_[0], p) && _[1] === t && _[2] === i ? _ : [p, t, i]; + }, + void 0, + () => [t.get(i), t, i] + ); + let L = n; + if ((s !== t || l !== i) && (c(), L = t.get(i)), v.useEffect(() => { + const _ = t.sub(i, () => { + c(); + }); + return c(), _; + }, [t, i, void 0]), v.useDebugValue(L), nE(L)) { + const _ = Vb(L, () => t.get(i)); + return Yb(_); + } + return L; + } + function E0(i, e) { + const t = uc(); + return v.useCallback( + (...s) => { + if ((Gb ? "production" : void 0) !== "production" && !("write" in i)) + throw new Error("not writable atom"); + return t.set(i, ...s); + }, + [t, i] + ); + } + function c4(i, e) { + return [ + j2(i), + // We do wrong type assertion here, which results in throwing an error. + E0(i) + ]; + } + function C0(i, e) { + if (!Array.isArray(i) || !Array.isArray(e)) + return false; + if (i === e) + return true; + if (i.length !== e.length) + return false; + for (let t = 0; t < i.length; t++) + if (i[t] !== e[t] && (!i[t].equals || !i[t].equals(e[t]))) + return false; + return true; + } + function rE(i) { + return i ? i.hashCode() : -1; + } + function aE(i, e) { + return i ? i.equals(e) : i === e; + } + function qb(i) { + return i === null ? "null" : i; + } + function It(i) { + return Array.isArray(i) ? "[" + i.map(qb).join(", ") + "]" : "null"; + } + function sE(i) { + const e = []; + return i.values().map(function(t) { + t instanceof g1.PrecedencePredicate && e.push(t); + }), e; + } + function S_(i, e) { + if (i === null) { + const t = { state: null, alt: null, context: null, semanticContext: null }; + return e && (t.reachesIntoOuterContext = 0), t; + } else { + const t = {}; + return t.state = i.state || null, t.alt = i.alt === void 0 ? null : i.alt, t.context = i.context || null, t.semanticContext = i.semanticContext || null, e && (t.reachesIntoOuterContext = i.reachesIntoOuterContext || 0, t.precedenceFilterSuppressed = i.precedenceFilterSuppressed || false), t; + } + } + function Kb(i, e) { + return i = i.replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\r/g, "\\r"), i; + } + function _c(i, e) { + if (e == null && (e = _0.EMPTY), e.parentCtx === null || e === _0.EMPTY) + return t1.EMPTY; + const t = _c(i, e.parentCtx), s = i.states[e.invokingState].transitions[0]; + return We.create(t, s.followState.stateNumber); + } + function cE(i, e, t) { + if (i.isEmpty()) + return i; + let n = t.get(i) || null; + if (n !== null) + return n; + if (n = e.get(i), n !== null) + return t.set(i, n), n; + let s = false, l = []; + for (let L = 0; L < l.length; L++) { + const d = cE(i.getParent(L), e, t); + if (s || d !== i.getParent(L)) { + if (!s) { + l = []; + for (let _ = 0; _ < i.length; _++) + l[_] = i.getParent(_); + s = true; + } + l[L] = d; + } + } + if (!s) + return e.add(i), t.set(i, i), i; + let c = null; + return l.length === 0 ? c = t1.EMPTY : l.length === 1 ? c = We.create(l[0], i.getReturnState(0)) : c = new x4(l, i.returnStates), e.add(c), t.set(c, c), t.set(i, c), c; + } + function gc(i, e, t, n) { + if (i === e) + return i; + if (i instanceof We && e instanceof We) + return Xb(i, e, t, n); + if (t) { + if (i instanceof Sl) + return i; + if (e instanceof Sl) + return e; + } + return i instanceof We && (i = new x4([i.getParent()], [i.returnState])), e instanceof We && (e = new x4([e.getParent()], [e.returnState])), Qb(i, e, t, n); + } + function Qb(i, e, t, n) { + if (n !== null) { + let p = n.get(i, e); + if (p !== null || (p = n.get(e, i), p !== null)) + return p; + } + let s = 0, l = 0, c = 0, L = [], d = []; + for (; s < i.returnStates.length && l < e.returnStates.length; ) { + const p = i.parents[s], m = e.parents[l]; + if (i.returnStates[s] === e.returnStates[l]) { + const f = i.returnStates[s]; + f === t1.EMPTY_RETURN_STATE && p === null && m === null || p !== null && m !== null && p === m ? (d[c] = p, L[c] = f) : (d[c] = gc(p, m, t, n), L[c] = f), s += 1, l += 1; + } else i.returnStates[s] < e.returnStates[l] ? (d[c] = p, L[c] = i.returnStates[s], s += 1) : (d[c] = m, L[c] = e.returnStates[l], l += 1); + c += 1; + } + if (s < i.returnStates.length) + for (let p = s; p < i.returnStates.length; p++) + d[c] = i.parents[p], L[c] = i.returnStates[p], c += 1; + else + for (let p = l; p < e.returnStates.length; p++) + d[c] = e.parents[p], L[c] = e.returnStates[p], c += 1; + if (c < d.length) { + if (c === 1) { + const p = We.create( + d[0], + L[0] + ); + return n !== null && n.set(i, e, p), p; + } + d = d.slice(0, c), L = L.slice(0, c); + } + const _ = new x4(d, L); + return _ === i ? (n !== null && n.set(i, e, i), i) : _ === e ? (n !== null && n.set(i, e, e), e) : (jb(d), n !== null && n.set(i, e, _), _); + } + function jb(i) { + const e = new nn(); + for (let t = 0; t < i.length; t++) { + const n = i[t]; + e.containsKey(n) || e.set(n, n); + } + for (let t = 0; t < i.length; t++) + i[t] = e.get(i[t]); + } + function Xb(i, e, t, n) { + if (n !== null) { + let l = n.get(i, e); + if (l !== null || (l = n.get(e, i), l !== null)) + return l; + } + const s = Jb(i, e, t); + if (s !== null) + return n !== null && n.set(i, e, s), s; + if (i.returnState === e.returnState) { + const l = gc(i.parentCtx, e.parentCtx, t, n); + if (l === i.parentCtx) + return i; + if (l === e.parentCtx) + return e; + const c = We.create(l, i.returnState); + return n !== null && n.set(i, e, c), c; + } else { + let l = null; + if ((i === e || i.parentCtx !== null && i.parentCtx === e.parentCtx) && (l = i.parentCtx), l !== null) { + const _ = [i.returnState, e.returnState]; + i.returnState > e.returnState && (_[0] = e.returnState, _[1] = i.returnState); + const p = [l, l], m = new x4(p, _); + return n !== null && n.set(i, e, m), m; + } + const c = [i.returnState, e.returnState]; + let L = [i.parentCtx, e.parentCtx]; + i.returnState > e.returnState && (c[0] = e.returnState, c[1] = i.returnState, L = [e.parentCtx, i.parentCtx]); + const d = new x4(L, c); + return n !== null && n.set(i, e, d), d; + } + } + function Jb(i, e, t) { + if (t) { + if (i === t1.EMPTY || e === t1.EMPTY) + return t1.EMPTY; + } else { + if (i === t1.EMPTY && e === t1.EMPTY) + return t1.EMPTY; + if (i === t1.EMPTY) { + const n = [ + e.returnState, + t1.EMPTY_RETURN_STATE + ], s = [e.parentCtx, null]; + return new x4(s, n); + } else if (e === t1.EMPTY) { + const n = [i.returnState, t1.EMPTY_RETURN_STATE], s = [i.parentCtx, null]; + return new x4(s, n); + } + } + return null; + } + function R8(i, e) { + const t = []; + return t[i - 1] = e, t.map(function(n) { + return e; + }); + } + function rx(i) { + return i.hashCodeForConfigSet(); + } + function ax(i, e) { + return i === e ? true : i === null || e === null ? false : i.equalsForConfigSet(e); + } + function M_(i) { + i.index = -1, i.line = 0, i.column = -1, i.dfaState = null; + } + function gx(i, e) { + return e !== null ? e : "failed predicate: {" + i + "}?"; + } + function VE(i) { + try { + if (i == null) return false; + const e = this.start.start, t = this.Body().stop.stop + 1; + return i >= e && i <= t; + } catch { + return false; + } + } + function ou(i) { + for (; i; ) { + if (i instanceof T0 || i instanceof su) + return i.Owner(); + i = i.parentCtx; + } + } + function tS() { + if (I_) return ar; + I_ = 1; + function i(e, t) { + switch (e) { + case 0: + return function() { + return t.apply(this, arguments); + }; + case 1: + return function(n) { + return t.apply(this, arguments); + }; + case 2: + return function(n, s) { + return t.apply(this, arguments); + }; + case 3: + return function(n, s, l) { + return t.apply(this, arguments); + }; + case 4: + return function(n, s, l, c) { + return t.apply(this, arguments); + }; + case 5: + return function(n, s, l, c, L) { + return t.apply(this, arguments); + }; + case 6: + return function(n, s, l, c, L, d) { + return t.apply(this, arguments); + }; + case 7: + return function(n, s, l, c, L, d, _) { + return t.apply(this, arguments); + }; + case 8: + return function(n, s, l, c, L, d, _, p) { + return t.apply(this, arguments); + }; + case 9: + return function(n, s, l, c, L, d, _, p, m) { + return t.apply(this, arguments); + }; + case 10: + return function(n, s, l, c, L, d, _, p, m, f) { + return t.apply(this, arguments); + }; + default: + throw new Error("First argument to _arity must be a non-negative integer no greater than ten"); + } + } + __name(i, "i"); + return ar = i, ar; + } + function aw() { + if (D_) return sr; + D_ = 1; + function i(e, t) { + return function() { + return t.call(this, e.apply(this, arguments)); + }; + } + __name(i, "i"); + return sr = i, sr; + } + function cu() { + if (k_) return or; + k_ = 1; + function i(e) { + return e != null && typeof e == "object" && e["@@functional/placeholder"] === true; + } + __name(i, "i"); + return or = i, or; + } + function b6() { + if (P_) return lr; + P_ = 1; + var i = /* @__PURE__ */ cu(); + function e(t) { + return /* @__PURE__ */ __name(function n(s) { + return arguments.length === 0 || i(s) ? n : t.apply(this, arguments); + }, "n"); + } + __name(e, "e"); + return lr = e, lr; + } + function nS() { + if (F_) return cr; + F_ = 1; + var i = /* @__PURE__ */ b6(), e = /* @__PURE__ */ cu(); + function t(n) { + return /* @__PURE__ */ __name(function s(l, c) { + switch (arguments.length) { + case 0: + return s; + case 1: + return e(l) ? s : i(function(L) { + return n(l, L); + }); + default: + return e(l) && e(c) ? s : e(l) ? i(function(L) { + return n(L, c); + }) : e(c) ? i(function(L) { + return n(l, L); + }) : n(l, c); + } + }, "s"); + } + __name(t, "t"); + return cr = t, cr; + } + function uu() { + if (U_) return ur; + U_ = 1; + var i = /* @__PURE__ */ b6(), e = /* @__PURE__ */ nS(), t = /* @__PURE__ */ cu(); + function n(s) { + return /* @__PURE__ */ __name(function l(c, L, d) { + switch (arguments.length) { + case 0: + return l; + case 1: + return t(c) ? l : e(function(_, p) { + return s(c, _, p); + }); + case 2: + return t(c) && t(L) ? l : t(c) ? e(function(_, p) { + return s(_, L, p); + }) : t(L) ? e(function(_, p) { + return s(c, _, p); + }) : i(function(_) { + return s(c, L, _); + }); + default: + return t(c) && t(L) && t(d) ? l : t(c) && t(L) ? e(function(_, p) { + return s(_, p, d); + }) : t(c) && t(d) ? e(function(_, p) { + return s(_, L, p); + }) : t(L) && t(d) ? e(function(_, p) { + return s(c, _, p); + }) : t(c) ? i(function(_) { + return s(_, L, d); + }) : t(L) ? i(function(_) { + return s(c, _, d); + }) : t(d) ? i(function(_) { + return s(c, L, _); + }) : s(c, L, d); + } + }, "l"); + } + __name(n, "n"); + return ur = n, ur; + } + function iS() { + return Z_ || (Z_ = 1, Lr = Array.isArray || function(e) { + return e != null && e.length >= 0 && Object.prototype.toString.call(e) === "[object Array]"; + }), Lr; + } + function sw() { + if (B_) return dr; + B_ = 1; + function i(e) { + return Object.prototype.toString.call(e) === "[object String]"; + } + __name(i, "i"); + return dr = i, dr; + } + function ow() { + if (G_) return Cr; + G_ = 1; + var i = /* @__PURE__ */ b6(), e = /* @__PURE__ */ iS(), t = /* @__PURE__ */ sw(), n = /* @__PURE__ */ i(function(l) { + return e(l) ? true : !l || typeof l != "object" || t(l) ? false : l.length === 0 ? true : l.length > 0 ? l.hasOwnProperty(0) && l.hasOwnProperty(l.length - 1) : false; + }); + return Cr = n, Cr; + } + function lw() { + if (z_) return _r; + z_ = 1; + var i = /* @__PURE__ */ function() { + function t(n) { + this.f = n; + } + __name(t, "t"); + return t.prototype["@@transducer/init"] = function() { + throw new Error("init not implemented on XWrap"); + }, t.prototype["@@transducer/result"] = function(n) { + return n; + }, t.prototype["@@transducer/step"] = function(n, s) { + return this.f(n, s); + }, t; + }(); + function e(t) { + return new i(t); + } + __name(e, "e"); + return _r = e, _r; + } + function cw() { + if (H_) return gr; + H_ = 1; + var i = /* @__PURE__ */ tS(), e = /* @__PURE__ */ nS(), t = /* @__PURE__ */ e(function(s, l) { + return i(s.length, function() { + return s.apply(l, arguments); + }); + }); + return gr = t, gr; + } + function uw() { + if (Y_) return pr; + Y_ = 1; + var i = /* @__PURE__ */ ow(), e = /* @__PURE__ */ lw(), t = /* @__PURE__ */ cw(); + function n(d, _, p) { + for (var m = 0, f = p.length; m < f; ) { + if (_ = d["@@transducer/step"](_, p[m]), _ && _["@@transducer/reduced"]) { + _ = _["@@transducer/value"]; + break; + } + m += 1; + } + return d["@@transducer/result"](_); + } + __name(n, "n"); + function s(d, _, p) { + for (var m = p.next(); !m.done; ) { + if (_ = d["@@transducer/step"](_, m.value), _ && _["@@transducer/reduced"]) { + _ = _["@@transducer/value"]; + break; + } + m = p.next(); + } + return d["@@transducer/result"](_); + } + __name(s, "s"); + function l(d, _, p, m) { + return d["@@transducer/result"](p[m](t(d["@@transducer/step"], d), _)); + } + __name(l, "l"); + var c = typeof Symbol < "u" ? Symbol.iterator : "@@iterator"; + function L(d, _, p) { + if (typeof d == "function" && (d = e(d)), i(p)) + return n(d, _, p); + if (typeof p["fantasy-land/reduce"] == "function") + return l(d, _, p, "fantasy-land/reduce"); + if (p[c] != null) + return s(d, _, p[c]()); + if (typeof p.next == "function") + return s(d, _, p); + if (typeof p.reduce == "function") + return l(d, _, p, "reduce"); + throw new TypeError("reduce: list must be array or iterable"); + } + __name(L, "L"); + return pr = L, pr; + } + function Lw() { + if (V_) return mr; + V_ = 1; + var i = /* @__PURE__ */ uu(), e = /* @__PURE__ */ uw(), t = /* @__PURE__ */ i(e); + return mr = t, mr; + } + function rS() { + if (q_) return hr; + q_ = 1; + var i = /* @__PURE__ */ iS(); + function e(t, n) { + return function() { + var s = arguments.length; + if (s === 0) + return n(); + var l = arguments[s - 1]; + return i(l) || typeof l[t] != "function" ? n.apply(this, arguments) : l[t].apply(l, Array.prototype.slice.call(arguments, 0, s - 1)); + }; + } + __name(e, "e"); + return hr = e, hr; + } + function dw() { + if (W_) return fr; + W_ = 1; + var i = /* @__PURE__ */ rS(), e = /* @__PURE__ */ uu(), t = /* @__PURE__ */ e( + /* @__PURE__ */ i("slice", function(s, l, c) { + return Array.prototype.slice.call(c, s, l); + }) + ); + return fr = t, fr; + } + function Cw() { + if ($_) return Er; + $_ = 1; + var i = /* @__PURE__ */ rS(), e = /* @__PURE__ */ b6(), t = /* @__PURE__ */ dw(), n = /* @__PURE__ */ e( + /* @__PURE__ */ i( + "tail", + /* @__PURE__ */ t(1, 1 / 0) + ) + ); + return Er = n, Er; + } + function _w() { + if (K_) return Sr; + K_ = 1; + var i = /* @__PURE__ */ tS(), e = /* @__PURE__ */ aw(), t = /* @__PURE__ */ Lw(), n = /* @__PURE__ */ Cw(); + function s() { + if (arguments.length === 0) + throw new Error("pipe requires at least one argument"); + return i(arguments[0].length, t(e, arguments[0], n(arguments))); + } + __name(s, "s"); + return Sr = s, Sr; + } + function mw() { + if (Q_) return Tr; + Q_ = 1; + var i = /* @__PURE__ */ uu(), e = /* @__PURE__ */ i(function(n, s, l) { + return l.replace(n, s); + }); + return Tr = e, Tr; + } + function ww(i) { + const e = new $.InputStream(i), t = new S2(e), n = new $.CommonTokenStream(t), s = new T(n); + return s.addErrorListener(new xw()), s._syntaxErrors ? null : s.prog(); + } + function Aw(i, e) { + const t = `WidthProviderOnBrowser_${i}_${e}`, n = Il(t); + if (n != null) + return n; + let s = document.querySelector( + ".textarea-hidden-div" + ); + if (!s) { + const c = document.createElement("div"); + c.className = "textarea-hidden-div ", c.style.fontSize = e === j8.MessageContent ? "0.875rem" : "1rem", c.style.fontFamily = "Helvetica, Verdana, serif", c.style.display = "inline", c.style.whiteSpace = "nowrap", c.style.visibility = "hidden", c.style.position = "absolute", c.style.top = "0", c.style.left = "0", c.style.overflow = "hidden", c.style.width = "0px", c.style.paddingLeft = "0px", c.style.paddingRight = "0px", c.style.margin = "0px", c.style.border = "0px", document.body.appendChild(c), s = c; + } + s.textContent = i; + const l = s.scrollWidth; + return Dl(t, l, true), l; + } + function x6(i) { + const e = $.tree.ParseTreeWalker.DEFAULT, t = new Dw(); + return e.walk(t, i), t.result(); + } + function Pw(i) { + const e = Be.getParticipants(i), t = Array.from(e.participants.entries()), n = x6(i), s = n.length === 0 && t.length === 0, l = n.some((L) => !L.from); + return (s || l) && t.unshift([ + Ze, + { ...zE, name: Ze, isStarter: true } + ]), t.map((L, d, _) => { + const p = L[1], m = d > 0 ? _[d - 1][1].name : ""; + return new kw( + p.name, + m, + p.label, + p.type + ); + }); + } + function w6(i, e) { + return { position: i, velocity: e }; + } + function eg(i, e) { + return w6(i.position + e.position, i.velocity + e.velocity); + } + function Fw(i, e) { + const t = i.position - e.position; + return t < -tg || Math.abs(t) <= tg && i.velocity < e.velocity; + } + function Uw() { + return { + delta: 1 / 0, + dualLessThan: /* @__PURE__ */ __name(function(i, e) { + const t = Fw(i, e); + return t && ([i, e] = [e, i]), i.velocity < e.velocity && (this.delta = Math.min( + this.delta, + (i.position - e.position) / (e.velocity - i.velocity) + )), t; + }, "dualLessThan") + }; + } + function Zw(i, e) { + const t = Array(); + for (let n = 0; n < i; n++) { + t.push([]); + for (let s = 0; s < n; s++) + e[s][n] > 0 && t[n].push({ i: s, length: w6(e[s][n], 0) }); + } + return t; + } + function Bw(i, e) { + const t = Uw(); + let n = w6(0, 0); + const s = []; + for (let l = 0; l < i.length; l++) { + let c = null; + l > 0 && (n = eg(n, e[l - 1])); + for (const L of i[l]) { + const d = eg(s[L.i].maximum, L.length); + t.dualLessThan(n, d) && (c = L.i, n = d); + } + s.push({ argument: c, maximum: n }); + } + return [t.delta, s]; + } + function Gw(i, e, t) { + let n = i.length - 1; + for (; n > 0; ) { + const s = i[n].argument; + s !== null ? n = s : (n--, t[n].velocity = 0); + } + } + function zw(i, e) { + for (let t = 0; t < i.length; t++) + i[t].position += i[t].velocity * e; + } + function Hw(i) { + const e = []; + for (const t of i) + e.push(t.maximum.position); + return e; + } + function Yw(i) { + const e = i.length, t = Zw(e, i), n = []; + for (let s = 1; s < e; s++) + n.push(w6(0, 1)); + for (; ; ) { + const [s, l] = Bw(t, n); + if (s == 1 / 0) + return Hw(l); + l[e - 1].maximum.velocity > 0 ? Gw(l, t, n) : zw(n, s); + } + } + function $w(i, e) { + if (i.match(/^[a-z]+:\/\//i)) + return i; + if (i.match(/^\/\//)) + return window.location.protocol + i; + if (i.match(/^[a-z]+:/i)) + return i; + const t = document.implementation.createHTMLDocument(), n = t.createElement("base"), s = t.createElement("a"); + return t.head.appendChild(n), t.body.appendChild(s), e && (n.href = e), s.href = i, s.href; + } + function kt(i) { + const e = []; + for (let t = 0, n = i.length; t < n; t++) + e.push(i[t]); + return e; + } + function hS(i = {}) { + return r0 || (i.includeStyleProperties ? (r0 = i.includeStyleProperties, r0) : (r0 = kt(window.getComputedStyle(document.documentElement)), r0)); + } + function X8(i, e) { + const n = (i.ownerDocument.defaultView || window).getComputedStyle(i).getPropertyValue(e); + return n ? parseFloat(n.replace("px", "")) : 0; + } + function Qw(i) { + const e = X8(i, "border-left-width"), t = X8(i, "border-right-width"); + return i.clientWidth + e + t; + } + function jw(i) { + const e = X8(i, "border-top-width"), t = X8(i, "border-bottom-width"); + return i.clientHeight + e + t; + } + function fS(i, e = {}) { + const t = e.width || Qw(i), n = e.height || jw(i); + return { width: t, height: n }; + } + function Xw() { + let i, e; + try { + e = process; + } catch { + } + const t = e && e.env ? e.env.devicePixelRatio : null; + return t && (i = parseInt(t, 10), Number.isNaN(i) && (i = 1)), i || window.devicePixelRatio || 1; + } + function Jw(i) { + (i.width > A3 || i.height > A3) && (i.width > A3 && i.height > A3 ? i.width > i.height ? (i.height *= A3 / i.width, i.width = A3) : (i.width *= A3 / i.height, i.height = A3) : i.width > A3 ? (i.height *= A3 / i.width, i.width = A3) : (i.width *= A3 / i.height, i.height = A3)); + } + function eM(i, e = {}) { + return i.toBlob ? new Promise((t) => { + i.toBlob(t, e.type ? e.type : "image/png", e.quality ? e.quality : 1); + }) : new Promise((t) => { + const n = window.atob(i.toDataURL(e.type ? e.type : void 0, e.quality ? e.quality : void 0).split(",")[1]), s = n.length, l = new Uint8Array(s); + for (let c = 0; c < s; c += 1) + l[c] = n.charCodeAt(c); + t(new Blob([l], { + type: e.type ? e.type : "image/png" + })); + }); + } + function J8(i) { + return new Promise((e, t) => { + const n = new Image(); + n.onload = () => { + n.decode().then(() => { + requestAnimationFrame(() => e(n)); + }); + }, n.onerror = t, n.crossOrigin = "anonymous", n.decoding = "async", n.src = i; + }); + } + async function tM(i) { + return Promise.resolve().then(() => new XMLSerializer().serializeToString(i)).then(encodeURIComponent).then((e) => `data:image/svg+xml;charset=utf-8,${e}`); + } + async function nM(i, e, t) { + const n = "http://www.w3.org/2000/svg", s = document.createElementNS(n, "svg"), l = document.createElementNS(n, "foreignObject"); + return s.setAttribute("width", `${e}`), s.setAttribute("height", `${t}`), s.setAttribute("viewBox", `0 0 ${e} ${t}`), l.setAttribute("width", "100%"), l.setAttribute("height", "100%"), l.setAttribute("x", "0"), l.setAttribute("y", "0"), l.setAttribute("externalResourcesRequired", "true"), s.appendChild(l), l.appendChild(i), tM(s); + } + function iM(i) { + const e = i.getPropertyValue("content"); + return `${i.cssText} content: '${e.replace(/'|"/g, "")}';`; + } + function rM(i, e) { + return hS(e).map((t) => { + const n = i.getPropertyValue(t), s = i.getPropertyPriority(t); + return `${t}: ${n}${s ? " !important" : ""};`; + }).join(" "); + } + function aM(i, e, t, n) { + const s = `.${i}:${e}`, l = t.cssText ? iM(t) : rM(t, n); + return document.createTextNode(`${s}{${l}}`); + } + function rg(i, e, t, n) { + const s = window.getComputedStyle(i, t), l = s.getPropertyValue("content"); + if (l === "" || l === "none") + return; + const c = Kw(); + try { + e.className = `${e.className} ${c}`; + } catch { + return; + } + const L = document.createElement("style"); + L.appendChild(aM(c, t, s, n)), e.appendChild(L); + } + function sM(i, e, t) { + rg(i, e, ":before", t), rg(i, e, ":after", t); + } + function lM(i) { + const e = /\.([^./]*?)$/g.exec(i); + return e ? e[1] : ""; + } + function hu(i) { + const e = lM(i).toLowerCase(); + return oM[e] || ""; + } + function cM(i) { + return i.split(/,/)[1]; + } + function Pl(i) { + return i.search(/^(data:)/) !== -1; + } + function uM(i, e) { + return `data:${e};base64,${i}`; + } + async function ES(i, e, t) { + const n = await fetch(i, e); + if (n.status === 404) + throw new Error(`Resource "${n.url}" not found`); + const s = await n.blob(); + return new Promise((l, c) => { + const L = new FileReader(); + L.onerror = c, L.onloadend = () => { + try { + l(t({ res: n, result: L.result })); + } catch (d) { + c(d); + } + }, L.readAsDataURL(s); + }); + } + function LM(i, e, t) { + let n = i.replace(/\?.*/, ""); + return t && (n = i), /ttf|otf|eot|woff2?/i.test(n) && (n = n.replace(/.*\//, "")), e ? `[${e}]${n}` : n; + } + async function fu(i, e, t) { + const n = LM(i, e, t.includeQueryParams); + if (vr[n] != null) + return vr[n]; + t.cacheBust && (i += (/\?/.test(i) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime()); + let s; + try { + const l = await ES(i, t.fetchRequestInit, ({ res: c, result: L }) => (e || (e = c.headers.get("Content-Type") || ""), cM(L))); + s = uM(l, e); + } catch (l) { + s = t.imagePlaceholder || ""; + let c = `Failed to fetch resource: ${i}`; + l && (c = typeof l == "string" ? l : l.message), c && console.warn(c); + } + return vr[n] = s, s; + } + async function dM(i) { + const e = i.toDataURL(); + return e === "data:," ? i.cloneNode(false) : J8(e); + } + async function CM(i, e) { + if (i.currentSrc) { + const l = document.createElement("canvas"), c = l.getContext("2d"); + l.width = i.clientWidth, l.height = i.clientHeight, c == null || c.drawImage(i, 0, 0, l.width, l.height); + const L = l.toDataURL(); + return J8(L); + } + const t = i.poster, n = hu(t), s = await fu(t, n, e); + return J8(s); + } + async function _M(i, e) { + var t; + try { + if (!((t = i == null ? void 0 : i.contentDocument) === null || t === void 0) && t.body) + return await A6(i.contentDocument.body, e, true); + } catch { + } + return i.cloneNode(false); + } + async function gM(i, e) { + return d3(i, HTMLCanvasElement) ? dM(i) : d3(i, HTMLVideoElement) ? CM(i, e) : d3(i, HTMLIFrameElement) ? _M(i, e) : i.cloneNode(SS(i)); + } + async function mM(i, e, t) { + var n, s; + if (SS(e)) + return e; + let l = []; + return pM(i) && i.assignedNodes ? l = kt(i.assignedNodes()) : d3(i, HTMLIFrameElement) && (!((n = i.contentDocument) === null || n === void 0) && n.body) ? l = kt(i.contentDocument.body.childNodes) : l = kt(((s = i.shadowRoot) !== null && s !== void 0 ? s : i).childNodes), l.length === 0 || d3(i, HTMLVideoElement) || await l.reduce((c, L) => c.then(() => A6(L, t)).then((d) => { + d && e.appendChild(d); + }), Promise.resolve()), e; + } + function hM(i, e, t) { + const n = e.style; + if (!n) + return; + const s = window.getComputedStyle(i); + s.cssText ? (n.cssText = s.cssText, n.transformOrigin = s.transformOrigin) : hS(t).forEach((l) => { + let c = s.getPropertyValue(l); + l === "font-size" && c.endsWith("px") && (c = `${Math.floor(parseFloat(c.substring(0, c.length - 2))) - 0.1}px`), d3(i, HTMLIFrameElement) && l === "display" && c === "inline" && (c = "block"), l === "d" && e.getAttribute("d") && (c = `path(${e.getAttribute("d")})`), n.setProperty(l, c, s.getPropertyPriority(l)); + }); + } + function fM(i, e) { + d3(i, HTMLTextAreaElement) && (e.innerHTML = i.value), d3(i, HTMLInputElement) && e.setAttribute("value", i.value); + } + function EM(i, e) { + if (d3(i, HTMLSelectElement)) { + const t = e, n = Array.from(t.children).find((s) => i.value === s.getAttribute("value")); + n && n.setAttribute("selected", ""); + } + } + function SM(i, e, t) { + return d3(e, Element) && (hM(i, e, t), sM(i, e, t), fM(i, e), EM(i, e)), e; + } + async function TM(i, e) { + const t = i.querySelectorAll ? i.querySelectorAll("use") : []; + if (t.length === 0) + return i; + const n = {}; + for (let l = 0; l < t.length; l++) { + const L = t[l].getAttribute("xlink:href"); + if (L) { + const d = i.querySelector(L), _ = document.querySelector(L); + !d && _ && !n[L] && (n[L] = await A6(_, e, true)); + } + } + const s = Object.values(n); + if (s.length) { + const l = "http://www.w3.org/1999/xhtml", c = document.createElementNS(l, "svg"); + c.setAttribute("xmlns", l), c.style.position = "absolute", c.style.width = "0", c.style.height = "0", c.style.overflow = "hidden", c.style.display = "none"; + const L = document.createElementNS(l, "defs"); + c.appendChild(L); + for (let d = 0; d < s.length; d++) + L.appendChild(s[d]); + i.appendChild(c); + } + return i; + } + async function A6(i, e, t) { + return !t && e.filter && !e.filter(i) ? null : Promise.resolve(i).then((n) => gM(n, e)).then((n) => mM(i, n, e)).then((n) => SM(i, n, e)).then((n) => TM(n, e)); + } + function bM(i) { + const e = i.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); + return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`, "g"); + } + function xM(i) { + const e = []; + return i.replace(TS, (t, n, s) => (e.push(s), t)), e.filter((t) => !Pl(t)); + } + async function wM(i, e, t, n, s) { + try { + const l = t ? $w(e, t) : e, c = hu(e); + let L; + return s || (L = await fu(l, c, n)), i.replace(bM(e), `$1${L}$3`); + } catch { + } + return i; + } + function MM(i, { preferredFontFormat: e }) { + return e ? i.replace(RM, (t) => { + for (; ; ) { + const [n, , s] = vM.exec(t) || []; + if (!s) + return ""; + if (s === e) + return `src: ${n};`; + } + }) : i; + } + function vS(i) { + return i.search(TS) !== -1; + } + async function RS(i, e, t) { + if (!vS(i)) + return i; + const n = MM(i, t); + return xM(n).reduce((l, c) => l.then((L) => wM(L, c, e, t)), Promise.resolve(n)); + } + async function a0(i, e, t) { + var n; + const s = (n = e.style) === null || n === void 0 ? void 0 : n.getPropertyValue(i); + if (s) { + const l = await RS(s, null, t); + return e.style.setProperty(i, l, e.style.getPropertyPriority(i)), true; + } + return false; + } + async function AM(i, e) { + await a0("background", i, e) || await a0("background-image", i, e), await a0("mask", i, e) || await a0("-webkit-mask", i, e) || await a0("mask-image", i, e) || await a0("-webkit-mask-image", i, e); + } + async function yM(i, e) { + const t = d3(i, HTMLImageElement); + if (!(t && !Pl(i.src)) && !(d3(i, SVGImageElement) && !Pl(i.href.baseVal))) + return; + const n = t ? i.src : i.href.baseVal, s = await fu(n, hu(n), e); + await new Promise((l, c) => { + i.onload = l, i.onerror = e.onImageErrorHandler ? (...d) => { + try { + l(e.onImageErrorHandler(...d)); + } catch (_) { + c(_); + } + } : c; + const L = i; + L.decode && (L.decode = l), L.loading === "lazy" && (L.loading = "eager"), t ? (i.srcset = "", i.src = s) : i.href.baseVal = s; + }); + } + async function NM(i, e) { + const n = kt(i.childNodes).map((s) => bS(s, e)); + await Promise.all(n).then(() => i); + } + async function bS(i, e) { + d3(i, Element) && (await AM(i, e), await yM(i, e), await NM(i, e)); + } + function OM(i, e) { + const { style: t } = i; + e.backgroundColor && (t.backgroundColor = e.backgroundColor), e.width && (t.width = `${e.width}px`), e.height && (t.height = `${e.height}px`); + const n = e.style; + return n != null && Object.keys(n).forEach((s) => { + t[s] = n[s]; + }), i; + } + async function lg(i) { + let e = og[i]; + if (e != null) + return e; + const n = await (await fetch(i)).text(); + return e = { url: i, cssText: n }, og[i] = e, e; + } + async function cg(i, e) { + let t = i.cssText; + const n = /url\(["']?([^"')]+)["']?\)/g, l = (t.match(/url\([^)]+\)/g) || []).map(async (c) => { + let L = c.replace(n, "$1"); + return L.startsWith("https://") || (L = new URL(L, i.url).href), ES(L, e.fetchRequestInit, ({ result: d }) => (t = t.replace(c, `url(${d})`), [c, d])); + }); + return Promise.all(l).then(() => t); + } + function ug(i) { + if (i == null) + return []; + const e = [], t = /(\/\*[\s\S]*?\*\/)/gi; + let n = i.replace(t, ""); + const s = new RegExp("((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})", "gi"); + for (; ; ) { + const d = s.exec(n); + if (d === null) + break; + e.push(d[0]); + } + n = n.replace(s, ""); + const l = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi, c = "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})", L = new RegExp(c, "gi"); + for (; ; ) { + let d = l.exec(n); + if (d === null) { + if (d = L.exec(n), d === null) + break; + l.lastIndex = L.lastIndex; + } else + L.lastIndex = l.lastIndex; + e.push(d[0]); + } + return e; + } + async function IM(i, e) { + const t = [], n = []; + return i.forEach((s) => { + if ("cssRules" in s) + try { + kt(s.cssRules || []).forEach((l, c) => { + if (l.type === CSSRule.IMPORT_RULE) { + let L = c + 1; + const d = l.href, _ = lg(d).then((p) => cg(p, e)).then((p) => ug(p).forEach((m) => { + try { + s.insertRule(m, m.startsWith("@import") ? L += 1 : s.cssRules.length); + } catch (f) { + console.error("Error inserting rule from remote css", { + rule: m, + error: f + }); + } + })).catch((p) => { + console.error("Error loading remote css", p.toString()); + }); + n.push(_); + } + }); + } catch (l) { + const c = i.find((L) => L.href == null) || document.styleSheets[0]; + s.href != null && n.push(lg(s.href).then((L) => cg(L, e)).then((L) => ug(L).forEach((d) => { + c.insertRule(d, c.cssRules.length); + })).catch((L) => { + console.error("Error loading remote stylesheet", L); + })), console.error("Error inlining remote css file", l); + } + }), Promise.all(n).then(() => (i.forEach((s) => { + if ("cssRules" in s) + try { + kt(s.cssRules || []).forEach((l) => { + t.push(l); + }); + } catch (l) { + console.error(`Error while reading CSS rules from ${s.href}`, l); + } + }), t)); + } + function DM(i) { + return i.filter((e) => e.type === CSSRule.FONT_FACE_RULE).filter((e) => vS(e.style.getPropertyValue("src"))); + } + async function kM(i, e) { + if (i.ownerDocument == null) + throw new Error("Provided element is not within a Document"); + const t = kt(i.ownerDocument.styleSheets), n = await IM(t, e); + return DM(n); + } + function xS(i) { + return i.trim().replace(/["']/g, ""); + } + function PM(i) { + const e = /* @__PURE__ */ new Set(); + function t(n) { + (n.style.fontFamily || getComputedStyle(n).fontFamily).split(",").forEach((l) => { + e.add(xS(l)); + }), Array.from(n.children).forEach((l) => { + l instanceof HTMLElement && t(l); + }); + } + __name(t, "t"); + return t(i), e; + } + async function FM(i, e) { + const t = await kM(i, e), n = PM(i); + return (await Promise.all(t.filter((l) => n.has(xS(l.style.fontFamily))).map((l) => { + const c = l.parentStyleSheet ? l.parentStyleSheet.href : null; + return RS(l.cssText, c, e); + }))).join(` +`); + } + async function UM(i, e) { + const t = e.fontEmbedCSS != null ? e.fontEmbedCSS : e.skipFonts ? null : await FM(i, e); + if (t) { + const n = document.createElement("style"), s = document.createTextNode(t); + n.appendChild(s), i.firstChild ? i.insertBefore(n, i.firstChild) : i.appendChild(n); + } + } + async function Eu(i, e = {}) { + const { width: t, height: n } = fS(i, e), s = await A6(i, e, true); + return await UM(s, e), await bS(s, e), OM(s, e), await nM(s, t, n); + } + async function Su(i, e = {}) { + const { width: t, height: n } = fS(i, e), s = await Eu(i, e), l = await J8(s), c = document.createElement("canvas"), L = c.getContext("2d"), d = e.pixelRatio || Xw(), _ = e.canvasWidth || t, p = e.canvasHeight || n; + return c.width = _ * d, c.height = p * d, e.skipAutoScale || Jw(c), c.style.width = `${_}`, c.style.height = `${p}`, e.backgroundColor && (L.fillStyle = e.backgroundColor, L.fillRect(0, 0, c.width, c.height)), L.drawImage(l, 0, 0, c.width, c.height), c; + } + async function wS(i, e = {}) { + return (await Su(i, e)).toDataURL(); + } + async function ZM(i, e = {}) { + return (await Su(i, e)).toDataURL("image/jpeg", e.quality || 1); + } + async function BM(i, e = {}) { + const t = await Su(i, e); + return await eM(t); + } + function MS(i) { + var e, t, n = ""; + if (typeof i == "string" || typeof i == "number") n += i; + else if (typeof i == "object") if (Array.isArray(i)) { + var s = i.length; + for (e = 0; e < s; e++) i[e] && (t = MS(i[e])) && (n && (n += " "), n += t); + } else for (t in i) i[t] && (n && (n += " "), n += t); + return n; + } + function GM() { + for (var i, e, t = 0, n = "", s = arguments.length; t < s; t++) (i = arguments[t]) && (e = MS(i)) && (n && (n += " "), n += e); + return n; + } + function eA() { + let i = 0, e, t, n = ""; + for (; i < arguments.length; ) + (e = arguments[i++]) && (t = yS(e)) && (n && (n += " "), n += t); + return n; + } + function tA(i, ...e) { + let t, n, s, l = c; + function c(d) { + const _ = e.reduce((p, m) => m(p), i()); + return t = jM(_), n = t.cache.get, s = t.cache.set, l = L, L(d); + } + __name(c, "c"); + function L(d) { + const _ = n(d); + if (_) + return _; + const p = JM(d, t); + return s(d, p), p; + } + __name(L, "L"); + return function() { + return l(eA.apply(null, arguments)); + }; + } + function z2(...i) { + return MA(GM(i)); + } + function LZ(i) { + const e = v.useRef(null); + return FS(() => { + e.current = i; + }, [ + i + ]), v.useCallback((...t) => { + const n = e.current; + return n == null ? void 0 : n(...t); + }, []); + } + function dZ(i) { + return i !== null && typeof i == "object" && "nodeType" in i && typeof i.nodeType == "number"; + } + function CZ(i) { + return dZ(i) && i.nodeType === Node.DOCUMENT_FRAGMENT_NODE && "host" in i; + } + function Ru() { + return _Z; + } + function US(i, e) { + if (!Ru()) return e && i ? i.contains(e) : false; + if (!i || !e) return false; + let t = e; + for (; t !== null; ) { + if (t === i) return true; + t.tagName === "SLOT" && t.assignedSlot ? t = t.assignedSlot.parentNode : CZ(t) ? t = t.host : t = t.parentNode; + } + return false; + } + function ZS(i) { + return Ru() && i.target.shadowRoot && i.composedPath ? i.composedPath()[0] : i.target; + } + function gZ(i) { + var e; + return typeof window > "u" || window.navigator == null ? false : ((e = window.navigator.userAgentData) === null || e === void 0 ? void 0 : e.brands.some((t) => i.test(t.brand))) || i.test(window.navigator.userAgent); + } + function pZ(i) { + var e; + return typeof window < "u" && window.navigator != null ? i.test(((e = window.navigator.userAgentData) === null || e === void 0 ? void 0 : e.platform) || window.navigator.platform) : false; + } + function BS(i) { + let e = null; + return () => (e == null && (e = i()), e); + } + function GS() { + let i = v.useRef(/* @__PURE__ */ new Map()), e = v.useCallback((s, l, c, L) => { + let d = L != null && L.once ? (..._) => { + i.current.delete(c), c(..._); + } : c; + i.current.set(c, { + type: l, + eventTarget: s, + fn: d, + options: L + }), s.addEventListener(l, d, L); + }, []), t = v.useCallback((s, l, c, L) => { + var d; + let _ = ((d = i.current.get(c)) === null || d === void 0 ? void 0 : d.fn) || c; + s.removeEventListener(l, _, L), i.current.delete(c); + }, []), n = v.useCallback(() => { + i.current.forEach((s, l) => { + t(s.eventTarget, s.type, l, s.options); + }); + }, [ + t + ]); + return v.useEffect(() => n, [ + n + ]), { + addGlobalListener: e, + removeGlobalListener: t, + removeAllGlobalListeners: n + }; + } + function fZ(i) { + return i.mozInputSource === 0 && i.isTrusted ? true : hZ() && i.pointerType ? i.type === "click" && i.buttons === 1 : i.detail === 0 && !i.pointerType; + } + function EZ() { + if (yg) return ke; + yg = 1; + var i = cc(); + function e(d) { + var _ = "https://react.dev/errors/" + d; + if (1 < arguments.length) { + _ += "?args[]=" + encodeURIComponent(arguments[1]); + for (var p = 2; p < arguments.length; p++) + _ += "&args[]=" + encodeURIComponent(arguments[p]); + } + return "Minified React error #" + d + "; visit " + _ + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings."; + } + __name(e, "e"); + function t() { + } + __name(t, "t"); + var n = { + d: { + f: t, + r: /* @__PURE__ */ __name(function() { + throw Error(e(522)); + }, "r"), + D: t, + C: t, + L: t, + m: t, + X: t, + S: t, + M: t + }, + p: 0, + findDOMNode: null + }, s = Symbol.for("react.portal"); + function l(d, _, p) { + var m = 3 < arguments.length && arguments[3] !== void 0 ? arguments[3] : null; + return { + $$typeof: s, + key: m == null ? null : "" + m, + children: d, + containerInfo: _, + implementation: p + }; + } + __name(l, "l"); + var c = i.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; + function L(d, _) { + if (d === "font") return ""; + if (typeof _ == "string") + return _ === "use-credentials" ? _ : ""; + } + __name(L, "L"); + return ke.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = n, ke.createPortal = function(d, _) { + var p = 2 < arguments.length && arguments[2] !== void 0 ? arguments[2] : null; + if (!_ || _.nodeType !== 1 && _.nodeType !== 9 && _.nodeType !== 11) + throw Error(e(299)); + return l(d, _, null, p); + }, ke.flushSync = function(d) { + var _ = c.T, p = n.p; + try { + if (c.T = null, n.p = 2, d) return d(); + } finally { + c.T = _, n.p = p, n.d.f(); + } + }, ke.preconnect = function(d, _) { + typeof d == "string" && (_ ? (_ = _.crossOrigin, _ = typeof _ == "string" ? _ === "use-credentials" ? _ : "" : void 0) : _ = null, n.d.C(d, _)); + }, ke.prefetchDNS = function(d) { + typeof d == "string" && n.d.D(d); + }, ke.preinit = function(d, _) { + if (typeof d == "string" && _ && typeof _.as == "string") { + var p = _.as, m = L(p, _.crossOrigin), f = typeof _.integrity == "string" ? _.integrity : void 0, h = typeof _.fetchPriority == "string" ? _.fetchPriority : void 0; + p === "style" ? n.d.S( + d, + typeof _.precedence == "string" ? _.precedence : void 0, + { + crossOrigin: m, + integrity: f, + fetchPriority: h + } + ) : p === "script" && n.d.X(d, { + crossOrigin: m, + integrity: f, + fetchPriority: h, + nonce: typeof _.nonce == "string" ? _.nonce : void 0 + }); + } + }, ke.preinitModule = function(d, _) { + if (typeof d == "string") + if (typeof _ == "object" && _ !== null) { + if (_.as == null || _.as === "script") { + var p = L( + _.as, + _.crossOrigin + ); + n.d.M(d, { + crossOrigin: p, + integrity: typeof _.integrity == "string" ? _.integrity : void 0, + nonce: typeof _.nonce == "string" ? _.nonce : void 0 + }); + } + } else _ == null && n.d.M(d); + }, ke.preload = function(d, _) { + if (typeof d == "string" && typeof _ == "object" && _ !== null && typeof _.as == "string") { + var p = _.as, m = L(p, _.crossOrigin); + n.d.L(d, p, { + crossOrigin: m, + integrity: typeof _.integrity == "string" ? _.integrity : void 0, + nonce: typeof _.nonce == "string" ? _.nonce : void 0, + type: typeof _.type == "string" ? _.type : void 0, + fetchPriority: typeof _.fetchPriority == "string" ? _.fetchPriority : void 0, + referrerPolicy: typeof _.referrerPolicy == "string" ? _.referrerPolicy : void 0, + imageSrcSet: typeof _.imageSrcSet == "string" ? _.imageSrcSet : void 0, + imageSizes: typeof _.imageSizes == "string" ? _.imageSizes : void 0, + media: typeof _.media == "string" ? _.media : void 0 + }); + } + }, ke.preloadModule = function(d, _) { + if (typeof d == "string") + if (_) { + var p = L(_.as, _.crossOrigin); + n.d.m(d, { + as: typeof _.as == "string" && _.as !== "script" ? _.as : void 0, + crossOrigin: p, + integrity: typeof _.integrity == "string" ? _.integrity : void 0 + }); + } else n.d.m(d); + }, ke.requestFormReset = function(d) { + n.d.r(d); + }, ke.unstable_batchedUpdates = function(d, _) { + return d(_); + }, ke.useFormState = function(d, _, p) { + return c.H.useFormState(d, _, p); + }, ke.useFormStatus = function() { + return c.H.useHostTransitionStatus(); + }, ke.version = "19.1.0", ke; + } + function zS() { + if (Ng) return wr.exports; + Ng = 1; + function i() { + if (!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ > "u" || typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE != "function")) + try { + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i); + } catch (e) { + console.error(e); + } + } + __name(i, "i"); + return i(), wr.exports = EZ(), wr.exports; + } + function HS(i) { + let e = i; + return e.nativeEvent = i, e.isDefaultPrevented = () => e.defaultPrevented, e.isPropagationStopped = () => e.cancelBubble, e.persist = () => { + }, e; + } + function SZ(i, e) { + Object.defineProperty(i, "target", { + value: e + }), Object.defineProperty(i, "currentTarget", { + value: e + }); + } + function YS(i) { + let e = v.useRef({ + isFocused: false, + observer: null + }); + FS(() => { + const n = e.current; + return () => { + n.observer && (n.observer.disconnect(), n.observer = null); + }; + }, []); + let t = LZ((n) => { + i == null || i(n); + }); + return v.useCallback((n) => { + if (n.target instanceof HTMLButtonElement || n.target instanceof HTMLInputElement || n.target instanceof HTMLTextAreaElement || n.target instanceof HTMLSelectElement) { + e.current.isFocused = true; + let s = n.target, l = /* @__PURE__ */ __name((c) => { + if (e.current.isFocused = false, s.disabled) { + let L = HS(c); + t(L); + } + e.current.observer && (e.current.observer.disconnect(), e.current.observer = null); + }, "l"); + s.addEventListener("focusout", l, { + once: true + }), e.current.observer = new MutationObserver(() => { + if (e.current.isFocused && s.disabled) { + var c; + (c = e.current.observer) === null || c === void 0 || c.disconnect(); + let L = s === document.activeElement ? null : document.activeElement; + s.dispatchEvent(new FocusEvent("blur", { + relatedTarget: L + })), s.dispatchEvent(new FocusEvent("focusout", { + bubbles: true, + relatedTarget: L + })); + } + }), e.current.observer.observe(s, { + attributes: true, + attributeFilter: [ + "disabled" + ] + }); + } + }, [ + t + ]); + } + function xu(i, e) { + for (let t of zl) t(i, e); + } + function RZ(i) { + return !(i.metaKey || !mZ() && i.altKey || i.ctrlKey || i.key === "Control" || i.key === "Shift" || i.key === "Meta"); + } + function e6(i) { + T9 = true, RZ(i) && (un = "keyboard", xu("keyboard", i)); + } + function L0(i) { + un = "pointer", (i.type === "mousedown" || i.type === "pointerdown") && (T9 = true, xu("pointer", i)); + } + function VS(i) { + fZ(i) && (T9 = true, un = "virtual"); + } + function qS(i) { + i.target === window || i.target === document || TZ || !i.isTrusted || (!T9 && !Hl && (un = "virtual", xu("virtual", i)), T9 = false, Hl = false); + } + function WS() { + T9 = false, Hl = true; + } + function Yl(i) { + if (typeof window > "u" || G5.get(g9(i))) return; + const e = g9(i), t = zt(i); + let n = e.HTMLElement.prototype.focus; + e.HTMLElement.prototype.focus = function() { + T9 = true, n.apply(this, arguments); + }, t.addEventListener("keydown", e6, true), t.addEventListener("keyup", e6, true), t.addEventListener("click", VS, true), e.addEventListener("focus", qS, true), e.addEventListener("blur", WS, false), typeof PointerEvent < "u" && (t.addEventListener("pointerdown", L0, true), t.addEventListener("pointermove", L0, true), t.addEventListener("pointerup", L0, true)), e.addEventListener("beforeunload", () => { + $S(i); + }, { + once: true + }), G5.set(e, { + focus: n + }); + } + function bZ(i) { + const e = zt(i); + let t; + return e.readyState !== "loading" ? Yl(i) : (t = /* @__PURE__ */ __name(() => { + Yl(i); + }, "t"), e.addEventListener("DOMContentLoaded", t)), () => $S(i, t); + } + function KS() { + return un !== "pointer"; + } + function wZ(i, e, t) { + let n = zt(t == null ? void 0 : t.target); + const s = typeof window < "u" ? g9(t == null ? void 0 : t.target).HTMLInputElement : HTMLInputElement, l = typeof window < "u" ? g9(t == null ? void 0 : t.target).HTMLTextAreaElement : HTMLTextAreaElement, c = typeof window < "u" ? g9(t == null ? void 0 : t.target).HTMLElement : HTMLElement, L = typeof window < "u" ? g9(t == null ? void 0 : t.target).KeyboardEvent : KeyboardEvent; + return i = i || n.activeElement instanceof s && !xZ.has(n.activeElement.type) || n.activeElement instanceof l || n.activeElement instanceof c && n.activeElement.isContentEditable, !(i && e === "keyboard" && t instanceof L && !vZ[t.key]); + } + function MZ(i, e, t) { + Yl(), v.useEffect(() => { + let n = /* @__PURE__ */ __name((s, l) => { + wZ(!!(t != null && t.isTextInput), s, l) && i(KS()); + }, "n"); + return zl.add(n), () => { + zl.delete(n); + }; + }, e); + } + function AZ(i) { + let { isDisabled: e, onFocus: t, onBlur: n, onFocusChange: s } = i; + const l = v.useCallback((d) => { + if (d.target === d.currentTarget) + return n && n(d), s && s(false), true; + }, [ + n, + s + ]), c = YS(l), L = v.useCallback((d) => { + const _ = zt(d.target), p = _ ? Gl(_) : Gl(); + d.target === d.currentTarget && p === ZS(d.nativeEvent) && (t && t(d), s && s(true), c(d)); + }, [ + s, + t, + c + ]); + return { + focusProps: { + onFocus: !e && (t || s || n) ? L : void 0, + onBlur: !e && (n || s) ? l : void 0 + } + }; + } + function yZ(i) { + let { isDisabled: e, onBlurWithin: t, onFocusWithin: n, onFocusWithinChange: s } = i, l = v.useRef({ + isFocusWithin: false + }), { addGlobalListener: c, removeAllGlobalListeners: L } = GS(), d = v.useCallback((m) => { + m.currentTarget.contains(m.target) && l.current.isFocusWithin && !m.currentTarget.contains(m.relatedTarget) && (l.current.isFocusWithin = false, L(), t && t(m), s && s(false)); + }, [ + t, + s, + l, + L + ]), _ = YS(d), p = v.useCallback((m) => { + if (!m.currentTarget.contains(m.target)) return; + const f = zt(m.target), h = Gl(f); + if (!l.current.isFocusWithin && h === ZS(m.nativeEvent)) { + n && n(m), s && s(true), l.current.isFocusWithin = true, _(m); + let R = m.currentTarget; + c(f, "focus", (b) => { + if (l.current.isFocusWithin && !US(R, b.target)) { + let M = new f.defaultView.FocusEvent("blur", { + relatedTarget: b.target + }); + SZ(M, R); + let w = HS(M); + d(w); + } + }, { + capture: true + }); + } + }, [ + n, + s, + _, + c, + d + ]); + return e ? { + focusWithinProps: { + // These cannot be null, that would conflict in mergeProps + onFocus: void 0, + onBlur: void 0 + } + } : { + focusWithinProps: { + onFocus: p, + onBlur: d + } + }; + } + function NZ() { + Vl = true, setTimeout(() => { + Vl = false; + }, 50); + } + function Og(i) { + i.pointerType === "touch" && NZ(); + } + function OZ() { + if (!(typeof document > "u")) + return typeof PointerEvent < "u" && document.addEventListener("pointerup", Og), Mr++, () => { + Mr--, !(Mr > 0) && typeof PointerEvent < "u" && document.removeEventListener("pointerup", Og); + }; + } + function QS(i) { + let { onHoverStart: e, onHoverChange: t, onHoverEnd: n, isDisabled: s } = i, [l, c] = v.useState(false), L = v.useRef({ + isHovered: false, + ignoreEmulatedMouseEvents: false, + pointerType: "", + target: null + }).current; + v.useEffect(OZ, []); + let { addGlobalListener: d, removeAllGlobalListeners: _ } = GS(), { hoverProps: p, triggerHoverEnd: m } = v.useMemo(() => { + let f = /* @__PURE__ */ __name((b, M) => { + if (L.pointerType = M, s || M === "touch" || L.isHovered || !b.currentTarget.contains(b.target)) return; + L.isHovered = true; + let w = b.currentTarget; + L.target = w, d(zt(b.target), "pointerover", (O) => { + L.isHovered && L.target && !US(L.target, O.target) && h(O, O.pointerType); + }, { + capture: true + }), e && e({ + type: "hoverstart", + target: w, + pointerType: M + }), t && t(true), c(true); + }, "f"), h = /* @__PURE__ */ __name((b, M) => { + let w = L.target; + L.pointerType = "", L.target = null, !(M === "touch" || !L.isHovered || !w) && (L.isHovered = false, _(), n && n({ + type: "hoverend", + target: w, + pointerType: M + }), t && t(false), c(false)); + }, "h"), R = {}; + return typeof PointerEvent < "u" && (R.onPointerEnter = (b) => { + Vl && b.pointerType === "mouse" || f(b, b.pointerType); + }, R.onPointerLeave = (b) => { + !s && b.currentTarget.contains(b.target) && h(b, b.pointerType); + }), { + hoverProps: R, + triggerHoverEnd: h + }; + }, [ + e, + t, + n, + s, + L, + d, + _ + ]); + return v.useEffect(() => { + s && m({ + currentTarget: L.target + }, L.pointerType); + }, [ + s + ]), { + hoverProps: p, + isHovered: l + }; + } + function jS(i = {}) { + let { autoFocus: e = false, isTextInput: t, within: n } = i, s = v.useRef({ + isFocused: false, + isFocusVisible: e || KS() + }), [l, c] = v.useState(false), [L, d] = v.useState(() => s.current.isFocused && s.current.isFocusVisible), _ = v.useCallback(() => d(s.current.isFocused && s.current.isFocusVisible), []), p = v.useCallback((h) => { + s.current.isFocused = h, c(h), _(); + }, [ + _ + ]); + MZ((h) => { + s.current.isFocusVisible = h, _(); + }, [], { + isTextInput: t + }); + let { focusProps: m } = AZ({ + isDisabled: n, + onFocusChange: p + }), { focusWithinProps: f } = yZ({ + isDisabled: !n, + onFocusWithinChange: p + }); + return { + isFocused: l, + isFocusVisible: L, + focusProps: n ? f : m + }; + } + function Ln(i) { + var e, t; + return m9.isServer ? null : i ? "ownerDocument" in i ? i.ownerDocument : "current" in i ? (t = (e = i.current) == null ? void 0 : e.ownerDocument) != null ? t : document : null : document; + } + function y6(i) { + typeof queueMicrotask == "function" ? queueMicrotask(i) : Promise.resolve().then(i).catch((e) => setTimeout(() => { + throw e; + })); + } + function Ht() { + let i = [], e = { addEventListener(t, n, s, l) { + return t.addEventListener(n, s, l), e.add(() => t.removeEventListener(n, s, l)); + }, requestAnimationFrame(...t) { + let n = requestAnimationFrame(...t); + return e.add(() => cancelAnimationFrame(n)); + }, nextFrame(...t) { + return e.requestAnimationFrame(() => e.requestAnimationFrame(...t)); + }, setTimeout(...t) { + let n = setTimeout(...t); + return e.add(() => clearTimeout(n)); + }, microTask(...t) { + let n = { current: true }; + return y6(() => { + n.current && t[0](); + }), e.add(() => { + n.current = false; + }); + }, style(t, n, s) { + let l = t.style.getPropertyValue(n); + return Object.assign(t.style, { [n]: s }), this.add(() => { + Object.assign(t.style, { [n]: l }); + }); + }, group(t) { + let n = Ht(); + return t(n), this.add(() => n.dispose()); + }, add(t) { + return i.includes(t) || i.push(t), () => { + let n = i.indexOf(t); + if (n >= 0) for (let s of i.splice(n, 1)) s(); + }; + }, dispose() { + for (let t of i.splice(0)) t(); + } }; + return e; + } + function N6() { + let [i] = v.useState(Ht); + return v.useEffect(() => () => i.dispose(), [i]), i; + } + function X4(i) { + let e = v.useRef(i); + return be(() => { + e.current = i; + }, [i]), e; + } + function O6() { + return v.useContext(PZ); + } + function ql(...i) { + return Array.from(new Set(i.flatMap((e) => typeof e == "string" ? e.split(" ") : []))).filter(Boolean).join(" "); + } + function $4(i, e, ...t) { + if (i in e) { + let s = e[i]; + return typeof s == "function" ? s(...t) : s; + } + let n = new Error(`Tried to handle "${i}" but there is no handler defined. Only defined handlers are: ${Object.keys(e).map((s) => `"${s}"`).join(", ")}.`); + throw Error.captureStackTrace && Error.captureStackTrace(n, $4), n; + } + function Ge() { + let i = UZ(); + return v.useCallback((e) => FZ({ mergeRefs: i, ...e }), [i]); + } + function FZ({ ourProps: i, theirProps: e, slot: t, defaultTag: n, features: s, visible: l = true, name: c, mergeRefs: L }) { + L = L ?? ZZ; + let d = XS(e, i); + if (l) return x8(d, t, n, c, L); + let _ = s ?? 0; + if (_ & 2) { + let { static: p = false, ...m } = d; + if (p) return x8(m, t, n, c, L); + } + if (_ & 1) { + let { unmount: p = true, ...m } = d; + return $4(p ? 0 : 1, { 0() { + return null; + }, 1() { + return x8({ ...m, hidden: true, style: { display: "none" } }, t, n, c, L); + } }); + } + return x8(d, t, n, c, L); + } + function x8(i, e = {}, t, n, s) { + let { as: l = t, children: c, refName: L = "ref", ...d } = yr(i, ["unmount", "static"]), _ = i.ref !== void 0 ? { [L]: i.ref } : {}, p = typeof c == "function" ? c(e) : c; + "className" in d && d.className && typeof d.className == "function" && (d.className = d.className(e)), d["aria-labelledby"] && d["aria-labelledby"] === d.id && (d["aria-labelledby"] = void 0); + let m = {}; + if (e) { + let f = false, h = []; + for (let [R, b] of Object.entries(e)) typeof b == "boolean" && (f = true), b === true && h.push(R.replace(/([A-Z])/g, (M) => `-${M.toLowerCase()}`)); + if (f) { + m["data-headlessui-state"] = h.join(" "); + for (let R of h) m[`data-${R}`] = ""; + } + } + if (l === v.Fragment && (Object.keys(yt(d)).length > 0 || Object.keys(yt(m)).length > 0)) if (!v.isValidElement(p) || Array.isArray(p) && p.length > 1) { + if (Object.keys(yt(d)).length > 0) throw new Error(['Passing props on "Fragment"!', "", `The current component <${n} /> is rendering a "Fragment".`, "However we need to passthrough the following props:", Object.keys(yt(d)).concat(Object.keys(yt(m))).map((f) => ` - ${f}`).join(` +`), "", "You can apply a few solutions:", ['Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".', "Render a single element as the child so that we can forward the props onto that element."].map((f) => ` - ${f}`).join(` +`)].join(` +`)); + } else { + let f = p.props, h = f == null ? void 0 : f.className, R = typeof h == "function" ? (...w) => ql(h(...w), d.className) : ql(h, d.className), b = R ? { className: R } : {}, M = XS(p.props, yt(yr(d, ["ref"]))); + for (let w in m) w in M && delete m[w]; + return v.cloneElement(p, Object.assign({}, M, m, _, { ref: s(BZ(p), _.ref) }, b)); + } + return v.createElement(l, Object.assign({}, yr(d, ["ref"]), l !== v.Fragment && _, l !== v.Fragment && m), p); + } + function UZ() { + let i = v.useRef([]), e = v.useCallback((t) => { + for (let n of i.current) n != null && (typeof n == "function" ? n(t) : n.current = t); + }, []); + return (...t) => { + if (!t.every((n) => n == null)) return i.current = t, e; + }; + } + function ZZ(...i) { + return i.every((e) => e == null) ? void 0 : (e) => { + for (let t of i) t != null && (typeof t == "function" ? t(e) : t.current = e); + }; + } + function XS(...i) { + if (i.length === 0) return {}; + if (i.length === 1) return i[0]; + let e = {}, t = {}; + for (let n of i) for (let s in n) s.startsWith("on") && typeof n[s] == "function" ? (t[s] != null || (t[s] = []), t[s].push(n[s])) : e[s] = n[s]; + if (e.disabled || e["aria-disabled"]) for (let n in t) /^(on(?:Click|Pointer|Mouse|Key)(?:Down|Up|Press)?)$/.test(n) && (t[n] = [(s) => { + var l; + return (l = s == null ? void 0 : s.preventDefault) == null ? void 0 : l.call(s); + }]); + for (let n in t) Object.assign(e, { [n](s, ...l) { + let c = t[n]; + for (let L of c) { + if ((s instanceof Event || (s == null ? void 0 : s.nativeEvent) instanceof Event) && s.defaultPrevented) return; + L(s, ...l); + } + } }); + return e; + } + function JS(...i) { + if (i.length === 0) return {}; + if (i.length === 1) return i[0]; + let e = {}, t = {}; + for (let n of i) for (let s in n) s.startsWith("on") && typeof n[s] == "function" ? (t[s] != null || (t[s] = []), t[s].push(n[s])) : e[s] = n[s]; + for (let n in t) Object.assign(e, { [n](...s) { + let l = t[n]; + for (let c of l) c == null || c(...s); + } }); + return e; + } + function xe(i) { + var e; + return Object.assign(v.forwardRef(i), { displayName: (e = i.displayName) != null ? e : i.name }); + } + function yt(i) { + let e = Object.assign({}, i); + for (let t in e) e[t] === void 0 && delete e[t]; + return e; + } + function yr(i, e = []) { + let t = Object.assign({}, i); + for (let n of e) n in t && delete t[n]; + return t; + } + function BZ(i) { + return I2.version.split(".")[0] >= "19" ? i.props.ref : i.ref; + } + function GZ(i, e, t) { + let [n, s] = v.useState(t), l = i !== void 0, c = v.useRef(l), L = v.useRef(false), d = v.useRef(false); + return l && !c.current && !L.current ? (L.current = true, c.current = l, console.error("A component is changing from uncontrolled to controlled. This may be caused by the value changing from undefined to a defined value, which should not happen.")) : !l && c.current && !d.current && (d.current = true, c.current = l, console.error("A component is changing from controlled to uncontrolled. This may be caused by the value changing from a defined value to undefined, which should not happen.")), [l ? i : n, f1((_) => (l || s(_), e == null ? void 0 : e(_)))]; + } + function zZ(i) { + let [e] = v.useState(i); + return e; + } + function eT(i = {}, e = null, t = []) { + for (let [n, s] of Object.entries(i)) nT(t, tT(e, n), s); + return t; + } + function tT(i, e) { + return i ? i + "[" + e + "]" : e; + } + function nT(i, e, t) { + if (Array.isArray(t)) for (let [n, s] of t.entries()) nT(i, tT(e, n.toString()), s); + else t instanceof Date ? i.push([e, t.toISOString()]) : typeof t == "boolean" ? i.push([e, t ? "1" : "0"]) : typeof t == "string" ? i.push([e, t]) : typeof t == "number" ? i.push([e, `${t}`]) : t == null ? i.push([e, ""]) : eT(t, e, i); + } + function HZ(i) { + var e, t; + let n = (e = i == null ? void 0 : i.form) != null ? e : i.closest("form"); + if (n) { + for (let s of n.elements) if (s !== i && (s.tagName === "INPUT" && s.type === "submit" || s.tagName === "BUTTON" && s.type === "submit" || s.nodeName === "INPUT" && s.type === "image")) { + s.click(); + return; + } + (t = n.requestSubmit) == null || t.call(n); + } + } + function VZ(i, e) { + var t; + let { features: n = 1, ...s } = i, l = { ref: e, "aria-hidden": (n & 2) === 2 ? true : (t = s["aria-hidden"]) != null ? t : void 0, hidden: (n & 4) === 4 ? true : void 0, style: { position: "fixed", top: 1, left: 1, width: 1, height: 0, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", borderWidth: "0", ...(n & 4) === 4 && (n & 2) !== 2 && { display: "none" } } }; + return Ge()({ ourProps: l, theirProps: s, slot: {}, defaultTag: YZ, name: "Hidden" }); + } + function WZ({ children: i }) { + let e = v.useContext(qZ); + if (!e) return I2.createElement(I2.Fragment, null, i); + let { target: t } = e; + return t ? bu.createPortal(I2.createElement(I2.Fragment, null, i), t) : null; + } + function $Z({ data: i, form: e, disabled: t, onReset: n, overrides: s }) { + let [l, c] = v.useState(null), L = N6(); + return v.useEffect(() => { + if (n && l) return L.addEventListener(l, "reset", n); + }, [l, e, n]), I2.createElement(WZ, null, I2.createElement(KZ, { setForm: c, formId: e }), eT(i).map(([d, _]) => I2.createElement(K5, { features: m0.Hidden, ...yt({ key: d, as: "input", type: "hidden", hidden: true, readOnly: true, form: e, disabled: t, name: d, value: _, ...s }) }))); + } + function KZ({ setForm: i, formId: e }) { + return v.useEffect(() => { + if (e) { + let t = document.getElementById(e); + t && i(t); + } + }, [i, e]), e ? null : I2.createElement(K5, { features: m0.Hidden, as: "input", type: "hidden", hidden: true, readOnly: true, ref: /* @__PURE__ */ __name((t) => { + if (!t) return; + let n = t.closest("form"); + n && i(n); + }, "ref") }); + } + function iT() { + return v.useContext(QZ); + } + function rT(i) { + let e = i.parentElement, t = null; + for (; e && !(e instanceof HTMLFieldSetElement); ) e instanceof HTMLLegendElement && (t = e), e = e.parentElement; + let n = (e == null ? void 0 : e.getAttribute("disabled")) === ""; + return n && jZ(t) ? false : n; + } + function jZ(i) { + if (!i) return false; + let e = i.previousElementSibling; + for (; e !== null; ) { + if (e instanceof HTMLLegendElement) return false; + e = e.previousElementSibling; + } + return true; + } + function XZ(i, e = true) { + return Object.assign(i, { [aT]: e }); + } + function _3(...i) { + let e = v.useRef(i); + v.useEffect(() => { + e.current = i; + }, [i]); + let t = f1((n) => { + for (let s of e.current) s != null && (typeof s == "function" ? s(n) : s.current = n); + }); + return i.every((n) => n == null || (n == null ? void 0 : n[aT])) ? void 0 : t; + } + function sT() { + let i = v.useContext(I6); + if (i === null) { + let e = new Error("You used a component, but it is not inside a relevant parent."); + throw Error.captureStackTrace && Error.captureStackTrace(e, sT), e; + } + return i; + } + function JZ() { + var i, e; + return (e = (i = v.useContext(I6)) == null ? void 0 : i.value) != null ? e : void 0; + } + function wu() { + let [i, e] = v.useState([]); + return [i.length > 0 ? i.join(" ") : void 0, v.useMemo(() => function(t) { + let n = f1((l) => (e((c) => [...c, l]), () => e((c) => { + let L = c.slice(), d = L.indexOf(l); + return d !== -1 && L.splice(d, 1), L; + }))), s = v.useMemo(() => ({ register: n, slot: t.slot, name: t.name, props: t.props, value: t.value }), [n, t.slot, t.name, t.props, t.value]); + return I2.createElement(I6.Provider, { value: s }, t.children); + }, [e])]; + } + function tB(i, e) { + let t = v.useId(), n = O6(), { id: s = `headlessui-description-${t}`, ...l } = i, c = sT(), L = _3(e); + be(() => c.register(s), [s, c.register]); + let d = n || false, _ = v.useMemo(() => ({ ...c.slot, disabled: d }), [c.slot, d]), p = { ref: L, ...c.props, id: s }; + return Ge()({ ourProps: p, theirProps: l, slot: _, defaultTag: eB, name: c.name || "Description" }); + } + function lT() { + let i = v.useContext(D6); + if (i === null) { + let e = new Error("You used a