commit f1502ed08c1ad7bca1732023bad5d8bfea2bca91 Author: renjue Date: Thu Jun 25 11:15:38 2026 +0800 初始化 ToolBox 开发者工具箱。 Co-authored-by: Cursor diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..66dc429 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +dist-ssr +.git +.gitea +.cursor +*.log +.DS_Store +.vscode +.idea +coverage +*.md +!README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6915891 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# 站点标题(浏览器标签、导航栏) +VITE_APP_TITLE=ToolBox + +# 备案号(留空则不显示;Docker 运行时可用 APP_ICP= 隐藏) +# VITE_APP_ICP= + +# Google Analytics 测量 ID(留空则不加载,避免向 Google 上报访问数据) +# VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX + +# 今日诗词 SDK 地址(留空则不加载,避免向第三方上报访问数据) +# VITE_JINRISHICI_SDK_URL=https://example.com/sdk.js diff --git a/.gitea/README.md b/.gitea/README.md new file mode 100644 index 0000000..5ac4542 --- /dev/null +++ b/.gitea/README.md @@ -0,0 +1,182 @@ +# Gitea Actions CI 模板(Go + Vue) + +复制整个 `.gitea/` 目录到新仓库即可启用 CI:**可选 go test** + **Docker 构建/推送**(Go 编译与 Vue 构建在 Dockerfile 内完成)。 + +## 项目约定 + +| 路径 | 说明 | +|------|------| +| `go.mod` | 仓库根目录 | +| `Dockerfile` | 仓库根目录,多阶段构建(含前端 `web/`) | +| `web/` | 可选;由 Dockerfile 内 `npm run build` 处理,CI 不再单独构建 | + +## Dockerfile 要求 + +工作流的 **Build image** 步骤在仓库根目录(或 Variable `DOCKERFILE` 指定路径)执行 `docker buildx build`,**不会**在 CI 里单独装 Node / 跑 `npm`。因此 Dockerfile 必须能独立完成构建与(运行时)启动。 + +### 基本要求 + +| 项 | 要求 | +|----|------| +| 位置 | 默认仓库根目录 `Dockerfile`;其他路径用 Variable `DOCKERFILE` | +| 构建上下文 | 仓库根目录 `.`(整个仓库会被 `COPY` 进镜像) | +| Go 版本 | `go.mod` 里 `go` 行与 `golang` 基础镜像大版本一致 | +| 多架构 | `go build` 须使用 `GOARCH="$TARGETARCH"`(buildx 注入);推荐 `CGO_ENABLED=0` | +| Go 模块代理 | **必须**在镜像内显式设置 `GOPROXY` / `GOSUMDB`(见下);容器内不会自动读取 `go.env` | +| 基础镜像加速 | 支持构建参数 `IMAGE_PREFIX`(CI 默认 `docker.1panel.live/library/`) | +| 前端(可选) | 存在 `web/` 时在 Dockerfile 内完成 `npm ci` + `npm run build` | + +### CI 自动传入的构建参数 + +| 参数 | 默认 | 说明 | +|------|------|------| +| `IMAGE_PREFIX` | `docker.1panel.live/library/` | 基础镜像前缀;`hub` 表示 Docker Hub | +| `GOPROXY` | `https://goproxy.cn,direct` | 与 workflow / Variable 一致 | +| `GOSUMDB` | `sum.golang.google.cn` | checksum 数据库 | + +buildx 还会注入 `TARGETARCH`、`BUILDPLATFORM` 等,无需在 workflow 里写。 + +### 推荐:Go + Vue 多阶段模板 + +```dockerfile +# syntax=docker/dockerfile:1 +ARG IMAGE_PREFIX= + +# 1) 前端(无 web/ 时可删整个 stage,并去掉 go-builder 里 COPY dist) +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder +WORKDIR /src/web +COPY web/package.json web/package-lock.json web/.npmrc ./ +RUN npm ci +COPY web/ ./ +RUN npm run build + +# 2) Go 编译 +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}golang:1.25-alpine AS go-builder +ARG TARGETARCH +ARG GOPROXY=https://goproxy.cn,direct +ARG GOSUMDB=sum.golang.google.cn +ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB} + +WORKDIR /src +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod go mod download + +COPY cmd/ ./cmd/ +COPY internal/ ./internal/ +# 若静态资源嵌入 Go:COPY --from=web-builder /src/web/dist/ ./path/to/static/ + +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" \ + go build -ldflags="-w -s" -o /app ./cmd/yourapp + +# 3) 运行镜像 +FROM ${IMAGE_PREFIX}alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +COPY --from=go-builder /app /usr/local/bin/yourapp +ENTRYPOINT ["/usr/local/bin/yourapp"] +``` + +按项目调整:`./cmd/yourapp`、静态资源路径、运行用户、`EXPOSE` / `VOLUME` 等。 + +### 仅 Go(无前端) + +删除 `web-builder` stage;`go-builder` 中不要 `COPY` 前端产物;其余 `GOPROXY` / `TARGETARCH` / `IMAGE_PREFIX` 要求相同。 + +### 前端 npm 源(国内) + +在 `web/.npmrc` 配置 registry,并在 Dockerfile 里与 `package.json` 一并 `COPY`: + +```ini +registry=https://registry.npmmirror.com +``` + +### 本地验证(与 CI 一致) + +```bash +docker buildx build --platform linux/amd64 \ + --build-arg IMAGE_PREFIX=docker.1panel.live/library/ \ + --build-arg GOPROXY=https://goproxy.cn,direct \ + -f Dockerfile -t myapp:test . +``` + +### 常见 Dockerfile 构建错误 + +| 报错 | 原因 | 处理 | +|------|------|------| +| `proxy.golang.org` timeout | 镜像内未设 `ENV GOPROXY` | go-builder 阶段加 `ARG`/`ENV GOPROXY` | +| `node` / Vite 版本不符 | 基础镜像 Node 过旧 | 使用 `node:20-alpine` 及以上 | +| 某架构 build 失败 | 未使用 `TARGETARCH` | `GOARCH="$TARGETARCH"` | +| 拉基础镜像 timeout | Hub 不可达 | `--build-arg IMAGE_PREFIX=docker.1panel.live/library/` 或 1Panel 配镜像加速 | + +## 一次性配置 + +1. **Runner**:部署 act_runner,标签含 `ubuntu-latest`,并挂载 `docker.sock`(见 [act-runner/README.md](act-runner/README.md))。 +2. **Secret**:仓库 Settings → Actions → Secrets,添加 `REGISTRY_TOKEN`(PAT,`write:package` 权限)。 +3. **Variable(推荐)**:若 runner 与 Gitea 同机、或 `gitea.server_url` 为内网地址,必须设置 `REGISTRY=你的公网域名`(如 `git.example.com`,**不要**填 `172.17.0.1:13827`)。 +4. **Gitea Registry**:服务端 `[packages] ENABLED = true`,`ROOT_URL` 正确;穿透场景建议 `PUBLIC_URL_DETECTION = never`(Gitea 1.26+)。 + +`REGISTRY` 未设置时,会从 `GITEA_ROOT_URL` 或 `gitea.server_url` 推断;均为内网地址时 workflow 会提前失败并提示。 + +## 可选 Variables + +仓库 Settings → Actions → Variables(留空则用默认值): + +| 变量 | 默认 | 说明 | +|------|------|------| +| `REGISTRY` | 见下方推断顺序 | **公网** Registry 主机名,如 `git.example.com`(勿用内网 IP:13827) | +| `GITEA_ROOT_URL` | (空) | 当 `gitea.server_url` 为内网时,可设 `https://git.example.com/` | +| `IMAGE_NAME` | 仓库名小写 | 镜像名,非 owner/repo 全路径 | +| `DOCKERFILE` | `Dockerfile` | Dockerfile 路径 | +| `DOCKER_PLATFORMS` | `linux/amd64,linux/arm64` | push 时 buildx 平台 | +| `GO_TEST_SCOPE` | `./...` | `go test` 包路径 | +| `RUN_GO_TEST` | (空,即运行) | 设 `false` 跳过 go test,仅 Docker 构建 | +| `DOCKER_IMAGE_PREFIX` | `1panel` | 基础镜像前缀;可选 `hub` / `daocloud` | +| `GOPROXY` | `https://goproxy.cn,direct` | Go 模块代理 | +| `GOSUMDB` | `sum.golang.google.cn` | Go checksum 数据库 | + +## 触发与镜像 tag + +- **pull_request**:go test(可关)+ 单架构 `docker build` 验证(不推送) +- **push main/master**:go test + 多架构构建并推送 `:latest`、`:sha-xxxxxxx` +- **push tag v\***:额外推送 `:v1.2.3` 等 + +示例(仓库 `rose_cat707/Prism`,Gitea 在 `git.example.com`): + +```text +git.example.com/rose_cat707/prism:latest +git.example.com/rose_cat707/prism:sha-35b3b48 +``` + +## 目录结构 + +```text +.gitea/ +├── README.md # 本文件 +├── workflows/ +│ └── ci.yml # 主工作流 +└── act-runner/ # runner 部署参考(可选) + ├── README.md + ├── config.yaml + ├── docker-compose.yml + └── .env.example +``` + +## 本地验证 Registry + +```bash +host=$(echo "https://你的-gitea-地址/" | sed -e 's|^https://||' -e 's|/.*||') +curl -s -D - "https://${host}/v2/" -o /dev/null | grep -i www-authenticate +docker pull "${host}/owner/image:latest" # 公开 Registry 无需 login +``` + +推送镜像(CI)仍需仓库 Secret `REGISTRY_TOKEN`(`write:package`)。仅拉取公开包不需要登录。 + +`realm` 应指向公网 Gitea 域名,而非 `127.0.0.1`。 + +## 常见 Registry 错误 + +| 报错 | 原因 | 处理 | +|------|------|------| +| `Get "https://172.17.0.1:13827/v2/"` HTTP/HTTPS | Variable `REGISTRY` 或 `gitea.server_url` 为内网地址 | 设 `REGISTRY=git.example.com` | +| token 指向 `127.0.0.1` | Gitea `realm` 配置错误 | `PUBLIC_URL_DETECTION=never` + 正确 `ROOT_URL` | diff --git a/.gitea/act-runner/.env.example b/.gitea/act-runner/.env.example new file mode 100644 index 0000000..06b3c00 --- /dev/null +++ b/.gitea/act-runner/.env.example @@ -0,0 +1,3 @@ +GITEA_INSTANCE_URL=https://git.example.com +GITEA_RUNNER_REGISTRATION_TOKEN=从仓库_Settings_Actions_Runners_复制 +GITEA_RUNNER_NAME=go-vue-ci-runner diff --git a/.gitea/act-runner/README.md b/.gitea/act-runner/README.md new file mode 100644 index 0000000..914e1cc --- /dev/null +++ b/.gitea/act-runner/README.md @@ -0,0 +1,183 @@ +# act_runner 配置参考 + +工作流 `runs-on: ubuntu-latest`;`docker` job 额外使用 `catthehacker/ubuntu:act-22.04` 容器(含 Docker CLI)。 + +## 构建镜像必须:挂载 Docker Socket + +`docker` job 需要访问宿主机 Docker 引擎。在 **runner 所在机器** 的 `config.yaml` 中配置: + +```yaml +container: + options: -v /var/run/docker.sock:/var/run/docker.sock + valid_volumes: + - /var/run/docker.sock +``` + +推荐 job 镜像(含 Node + Docker CLI): + +```yaml +runner: + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" + - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" +``` + +修改后 **重启 runner**(例如 `docker restart ` 或重启 Gitea Runner 服务)。 + +## 1Panel 宿主机:配置 Docker 镜像加速(推荐) + +Runner 通过 `docker.sock` 使用宿主机 Docker。在 **1Panel** 中配置加速器后,普通 `docker pull` 会走加速;**buildx 多架构构建**仍建议配合 workflow 内的 `IMAGE_PREFIX`(默认 `docker.1panel.live`)。 + +1. 登录 1Panel → **容器** → **配置** +2. **镜像加速地址** 填入: + ```text + https://docker.1panel.live + ``` +3. 保存并 **重启 Docker** + +验证(在 runner 宿主机): + +```bash +docker info | grep -A5 'Registry Mirrors' +docker pull alpine:3.20 +``` + +等价 `daemon.json`: + +```json +{ + "registry-mirrors": ["https://docker.1panel.live"] +} +``` + +CI 工作流默认 `IMAGE_PREFIX=docker.1panel.live/library/`(buildkit 拉基础镜像)。若 1Panel 加速不可用,可在仓库 Variables 设 `DOCKER_IMAGE_PREFIX=daocloud` 或 `hub`。 + +参考:[1Panel 容器配置文档](https://1panel.cn/docs/user_manual/containers/setting) + +## 验证 + +在 runner 宿主机执行: + +```bash +docker info +ls -l /var/run/docker.sock +``` + +## 从零部署 runner(可选) + +```bash +cd .gitea/act-runner +cp .env.example .env # 填入 Registration Token +docker compose up -d +``` + +## GitHub Actions 镜像(替代 ghfast) + +日志里出现 `git clone 'https://ghfast.top/https://github.com/actions/checkout'` 说明 **runner 宿主机** 的 `config.yaml` 配置了 `github_mirror`,与仓库 workflow 无关。 + +在 runner 的 `config.yaml` 中修改(修改后重启 runner): + +```yaml +runner: + github_mirror: 'https://gitea.com' # 推荐 +``` + +常用替代方案: + +| `github_mirror` 值 | 说明 | +|------------------|------| +| `''`(留空) | 直连 `github.com`,网络可达时最简单 | +| `https://gitea.com` | Gitea 官方 actions 镜像,国内较稳 | +| `https://gitclone.com/github.com` | 第三方 GitHub 克隆镜像 | +| `https://ghfast.top/https://github.com` | 部分环境需代理认证,易报 `Proxy Authentication Required` | + +前提:Gitea `app.ini` 中 `[actions] DEFAULT_ACTIONS_URL = github`(默认)。 + +**推荐写法**(与 Prism 等仓库一致,由 runner `github_mirror` 拉取 Action): + +```yaml +uses: actions/checkout@v4 +``` + +也可写 Gitea 镜像绝对 URL(不依赖 runner 配置): + +```yaml +uses: https://gitea.com/actions/checkout@v4 +``` + +勿写 `https://github.com/actions/checkout@v4`:会绕过 `github_mirror` 直连 GitHub,内网 runner 易报 `unexpected EOF`。 + +修改 `github_mirror` 后建议清理 runner 缓存目录(如 `/root/.cache/act`),否则旧缓存可能仍指向 ghfast。 + +## 常见错误 + +| 报错 | 原因 | 处理 | +|------|------|------| +| `registry-1.docker.io` i/o timeout | Docker Hub 不可达 | 1Panel 配 `docker.1panel.live`;Variable `DOCKER_IMAGE_PREFIX=1panel` | +| `ghfast.top` Proxy Authentication Required | runner `github_mirror` 指向 ghfast 且需代理认证 | 改 `github_mirror` 或删掉;见上文「GitHub Actions 镜像」 | +| `docker: command not found` | job 容器无 Docker CLI | 工作流已指定 act 镜像;或 runner 改用 catthehacker/ubuntu | +| `Cannot connect to Docker daemon` | 未挂载 docker.sock | 按上文修改 config.yaml 并重启 runner | +| `node not in PATH` | job 镜像无 Node | 标签映射改用 catthehacker/ubuntu:act-22.04 | +| `http: server gave HTTP response to HTTPS client` 且 token 指向 `127.0.0.1` | **Gitea Registry 配置/反代错误** | 见下文「Registry 登录失败」 | + +## Registry 登录失败(127.0.0.1 / HTTP vs HTTPS) + +若 `docker login git.rc707blog.top` 报错类似: + +```text +Get "https://127.0.0.1:xxxxx/v2/token?...": http: server gave HTTP response to HTTPS client +``` + +说明 Gitea 把 **Docker 认证 token 地址** 配成了本机内网地址,CI runner 访问不到。需在 **Gitea 服务器** 修复,而非改 workflow。 + +### 1. 检查 `app.ini` + +```ini +[server] +ROOT_URL = https://git.example.com/ +LOCAL_ROOT_URL = http://127.0.0.1:3000/ +; 内网穿透 / 错误 Host 时(Gitea 1.26+): +; PUBLIC_URL_DETECTION = never + +[packages] +ENABLED = true +``` + +`ROOT_URL` 必须与浏览器访问 Gitea 的 **HTTPS 外网地址** 完全一致(含末尾 `/`)。 + +### 2. 反向代理必须转发 `/v2` 并带上头 + +Container Registry 固定使用根路径 `/v2`。Nginx 示例: + +```nginx +location / { + client_max_body_size 0; + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +关键:`X-Forwarded-Proto: https` 和正确的 `Host`(与 Gitea `ROOT_URL` 域名一致)。 + +### 3. 在 runner 宿主机验证(修复后) + +```bash +GITEA_HOST=git.example.com # 改成你的 Gitea 域名 +curl -s -D - "https://${GITEA_HOST}/v2/" -o /dev/null | grep -i www-authenticate +echo "$REGISTRY_TOKEN" | docker login "${GITEA_HOST}" -u 你的用户名 --password-stdin +``` + +应返回 `401 Unauthorized`(正常,表示 registry 可达)且 `docker login` 显示 **Login Succeeded**。 + +参考:[Gitea 反向代理文档](https://docs.gitea.com/administration/reverse-proxies) + +## Gitea Runner v0.6.x(个人 runner) + +1. 找到 runner 的配置文件或环境(安装目录 / docker compose) +2. 确保 runner 进程能访问宿主机 `/var/run/docker.sock` +3. Runners 页标签含 `ubuntu-latest` 且状态 **空闲/在线** + +若使用 Gitea 网页注册的个人 runner(docker 模式),通常需在 runner 启动参数或 `config.yaml` 里加入 socket 挂载,具体路径取决于你的安装方式。 diff --git a/.gitea/act-runner/config.yaml b/.gitea/act-runner/config.yaml new file mode 100644 index 0000000..ba9616b --- /dev/null +++ b/.gitea/act-runner/config.yaml @@ -0,0 +1,33 @@ +# act_runner 配置(可选参考部署) +# 工作流 runs-on: ubuntu-latest + +log: + level: info + +runner: + file: .runner + capacity: 2 + timeout: 3h + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + # 拉取 uses: actions/checkout@v4 等 GitHub Action 时的镜像(替换 https://github.com) + # 需 Gitea app.ini 中 [actions] DEFAULT_ACTIONS_URL = github + # 留空则直连 github.com;第三方镜像不稳定时可改用 workflow 绝对 URL(见 .gitea/README.md) + # + # github_mirror: 'https://gitea.com' # 推荐:配合 uses: actions/checkout@v4 + # github_mirror: 'https://gitclone.com/github.com' # 第三方 GitHub 克隆镜像 + # github_mirror: 'https://ghfast.top/https://github.com' # 需代理认证时易失败,不推荐 + labels: + - "ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" + - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" + +cache: + enabled: false + +container: + network: bridge + privileged: false + options: -v /var/run/docker.sock:/var/run/docker.sock + valid_volumes: + - /var/run/docker.sock diff --git a/.gitea/act-runner/docker-compose.yml b/.gitea/act-runner/docker-compose.yml new file mode 100644 index 0000000..cd192f1 --- /dev/null +++ b/.gitea/act-runner/docker-compose.yml @@ -0,0 +1,22 @@ +# Gitea act_runner — 可选参考部署(标签 default,与工作流一致) +# +# 若已在 Gitea 注册个人/仓库 runner 且标签为 default,无需使用本目录。 + +services: + act-runner: + image: docker.io/gitea/act_runner:0.2.12 + container_name: prism-act-runner + restart: unless-stopped + environment: + CONFIG_FILE: /config.yaml + GITEA_INSTANCE_URL: ${GITEA_INSTANCE_URL:-https://git.rc707blog.top} + GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN} + GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-prism-ci-runner} + volumes: + - ./config.yaml:/config.yaml:ro + - act-runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + working_dir: /data + +volumes: + act-runner-data: diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a3f99d2 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,263 @@ +# ToolBox:Vue 前端 CI(Gitea Actions) +# +# 单 job:npm test + Docker 多架构构建/推送 +# 约定:Dockerfile 在仓库根目录 +# +# 前置:act_runner(ubuntu-latest + docker.sock)、Secret REGISTRY_TOKEN +# Variables:REGISTRY、IMAGE_NAME、DOCKER_IMAGE_PREFIX、RUN_NPM_TEST 等 + +name: CI + +on: + push: + branches: [main, master] + tags: ['v*'] + pull_request: + workflow_dispatch: + +env: + REGISTRY: ${{ vars.REGISTRY }} + GITEA_ROOT_URL: ${{ vars.GITEA_ROOT_URL }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + DOCKERFILE: ${{ vars.DOCKERFILE }} + DOCKER_PLATFORMS: ${{ vars.DOCKER_PLATFORMS }} + RUN_NPM_TEST: ${{ vars.RUN_NPM_TEST }} + DOCKER_IMAGE_PREFIX: ${{ vars.DOCKER_IMAGE_PREFIX }} + +jobs: + docker: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker + run: | + set -eux + if ! command -v docker >/dev/null 2>&1; then + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io + fi + if [ ! -S /var/run/docker.sock ]; then + echo "ERROR: /var/run/docker.sock 未挂载到 job 容器。" >&2 + echo "请在 act_runner config.yaml 中配置:" >&2 + echo " container.options: -v /var/run/docker.sock:/var/run/docker.sock" >&2 + echo " container.valid_volumes: [/var/run/docker.sock]" >&2 + exit 1 + fi + docker info + + - name: Run npm tests + if: vars.RUN_NPM_TEST != 'false' + shell: bash + run: | + set -euo pipefail + NODE_VERSION=20.20.2 + ARCH=$(uname -m | sed 's/x86_64/x64/; s/aarch64/arm64/') + TAR="node-v${NODE_VERSION}-linux-${ARCH}.tar.gz" + + if ! command -v node >/dev/null 2>&1 || ! node -v | grep -qE "v20\."; then + downloaded=0 + for url in \ + "https://npmmirror.com/mirrors/node/v${NODE_VERSION}/${TAR}" \ + "https://mirrors.aliyun.com/nodejs-release/v${NODE_VERSION}/${TAR}" \ + "https://nodejs.org/dist/v${NODE_VERSION}/${TAR}"; do + echo "trying ${url}" + if curl -fsSL --connect-timeout 20 --retry 3 --retry-delay 5 --max-time 600 \ + "$url" -o /tmp/node.tar.gz; then + downloaded=1 + break + fi + done + [ "$downloaded" -eq 1 ] || { echo "failed to download Node ${NODE_VERSION}" >&2; exit 1; } + rm -rf /usr/local/node + mkdir -p /usr/local/node + tar -xzf /tmp/node.tar.gz -C /usr/local/node --strip-components=1 + export PATH="/usr/local/node/bin:${PATH}" + fi + node -v + npm -v + + # act-runner 工作区 volume 可能残留旧 lockfile,强制与当前 commit 一致 + git checkout HEAD -- package-lock.json package.json .npmrc + echo "commit: $(git rev-parse --short HEAD)" + echo "lockfile sample:" + grep -m3 '"resolved"' package-lock.json || true + if grep -qE 'artifactory|xiaohongshu' package-lock.json; then + echo "ERROR: package-lock.json 仍含内网源,请检查 checkout" >&2 + exit 1 + fi + + # 清除 runner 注入的内网 npm 配置,使用独立缓存目录 + unset NPM_CONFIG_REGISTRY NPM_REGISTRY npm_config_registry NODE_AUTH_TOKEN 2>/dev/null || true + export CI=true + export NPM_CONFIG_USERCONFIG=/tmp/toolbox-npmrc + export NPM_CONFIG_GLOBALCONFIG=/tmp/toolbox-npmrc-global + export npm_config_cache=/tmp/toolbox-npm-cache + export npm_config_progress=true + export npm_config_loglevel=info + export npm_config_registry=https://registry.npmmirror.com + export npm_config_esbuild_binary_host_mirror=https://npmmirror.com/mirrors/esbuild + printf '%s\n' 'registry=https://registry.npmmirror.com' > /tmp/toolbox-npmrc-global + printf '%s\n' \ + 'registry=https://registry.npmmirror.com' \ + 'esbuild_binary_host_mirror=https://npmmirror.com/mirrors/esbuild' \ + 'always-auth=false' \ + > /tmp/toolbox-npmrc + rm -rf node_modules /tmp/toolbox-npm-cache + mkdir -p /tmp/toolbox-npm-cache + npm cache clean --force + echo "npm registry: $(npm config get registry)" + echo "=== npm ci ===" + npm ci --userconfig=/tmp/toolbox-npmrc --registry=https://registry.npmmirror.com + echo "=== npm test ===" + npm run test:run -- --pool=forks --maxWorkers=2 --reporter=verbose + + - name: Resolve registry + id: reg + if: gitea.event_name == 'push' + shell: bash + env: + GITEA_SERVER_URL: ${{ gitea.server_url }} + run: | + set -euo pipefail + + url_to_host() { + echo "$1" | sed -e 's|^https://||' -e 's|^http://||' -e 's|/.*||' + } + + is_internal_host() { + local host="${1%%:*}" + case "$host" in + localhost|127.*|10.*|192.168.*) return 0 ;; + 172.*) + local second + second=$(echo "$host" | cut -d. -f2) + [ "$second" -ge 16 ] && [ "$second" -le 31 ] + return + ;; + *) return 1 ;; + esac + } + + registry="${REGISTRY:-}" + if [ -z "$registry" ] && [ -n "${GITEA_ROOT_URL:-}" ]; then + registry=$(url_to_host "${GITEA_ROOT_URL}") + fi + if [ -z "$registry" ]; then + registry=$(url_to_host "${GITEA_SERVER_URL}") + fi + + if is_internal_host "$registry"; then + echo "ERROR: Registry 主机 '${registry}' 是内网地址。" >&2 + echo "请设置 Variable REGISTRY=公网 Gitea 域名(如 git.example.com)。" >&2 + exit 1 + fi + + echo "registry host: ${registry}" + + auth_header="" + if auth_header=$(curl -fsS -D - "https://${registry}/v2/" -o /dev/null 2>&1 | grep -i '^www-authenticate:'); then + if echo "$auth_header" | grep -qiE '127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|localhost|:13827'; then + echo "ERROR: Registry token realm 指向内网地址:${auth_header}" >&2 + exit 1 + fi + fi + + echo "host=${registry}" >> "${GITHUB_OUTPUT}" + + - name: Log in to Container Registry + if: gitea.event_name == 'push' + shell: bash + run: | + set -euo pipefail + echo "${{ secrets.REGISTRY_TOKEN }}" | \ + docker login "${{ steps.reg.outputs.host }}" -u "${{ gitea.actor }}" --password-stdin + + - name: Build image + shell: bash + env: + GITEA_SHA: ${{ gitea.sha }} + GITEA_REF: ${{ gitea.ref }} + GITEA_REPOSITORY: ${{ gitea.repository }} + GITEA_REPOSITORY_OWNER: ${{ gitea.repository_owner }} + GITEA_EVENT_NAME: ${{ gitea.event_name }} + REGISTRY_HOST: ${{ steps.reg.outputs.host }} + run: | + set -euo pipefail + + dockerfile="${DOCKERFILE:-Dockerfile}" + + if [ "${DOCKER_IMAGE_PREFIX:-}" = "hub" ] || [ "${DOCKER_IMAGE_PREFIX:-}" = "docker.io" ]; then + image_prefix="" + elif [ -z "${DOCKER_IMAGE_PREFIX:-}" ] || [ "${DOCKER_IMAGE_PREFIX}" = "1panel" ]; then + image_prefix="docker.1panel.live/library/" + elif [ "${DOCKER_IMAGE_PREFIX}" = "daocloud" ]; then + image_prefix="docker.m.daocloud.io/library/" + else + image_prefix="${DOCKER_IMAGE_PREFIX}" + [[ "${image_prefix}" == */ ]] || image_prefix="${image_prefix}/" + fi + echo "using IMAGE_PREFIX=${image_prefix:-}" + build_args=(--build-arg "IMAGE_PREFIX=${image_prefix}") + + builder_id="gitea-buildx-${GITEA_REPOSITORY//\//-}" + + if [ "${GITEA_EVENT_NAME}" = "push" ]; then + install_binfmt() { + for img in \ + "docker.1panel.live/tonistiigi/binfmt:latest" \ + "docker.m.daocloud.io/tonistiigi/binfmt:latest" \ + "tonistiigi/binfmt:latest"; do + echo "trying binfmt image: ${img}" + if docker run --privileged --rm "${img}" --install all; then + return 0 + fi + done + return 1 + } + install_binfmt || true + + registry="${REGISTRY_HOST}" + image_name="${IMAGE_NAME:-$(echo "${GITEA_REPOSITORY##*/}" | tr '[:upper:]' '[:lower:]')}" + platforms="${DOCKER_PLATFORMS:-linux/amd64,linux/arm64}" + image="${registry}/${GITEA_REPOSITORY_OWNER}/${image_name}" + tags="${image}:sha-${GITEA_SHA:0:7}" + case "${GITEA_REF}" in + refs/heads/main|refs/heads/master) + tags="${tags},${image}:latest" + ;; + refs/tags/v*) + tags="${tags},${image}:${GITEA_REF#refs/tags/}" + ;; + esac + + docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}" + docker buildx inspect --bootstrap + + tag_args=() + IFS=',' read -r -a tag_list <<< "$tags" + for t in "${tag_list[@]}"; do + tag_args+=(-t "$t") + done + + docker buildx build \ + --platform "${platforms}" \ + -f "${dockerfile}" \ + "${build_args[@]}" \ + "${tag_args[@]}" \ + --push \ + . + else + # PR:仅单架构构建验证 Dockerfile,不推送 + docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}" + docker buildx inspect --bootstrap + docker buildx build \ + --platform linux/amd64 \ + -f "${dockerfile}" \ + "${build_args[@]}" \ + --load \ + . + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2748ef7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.cursor +coverage +.env +.env.* +!.env.example diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..dd1eedc --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmmirror.com +esbuild_binary_host_mirror=https://npmmirror.com/mirrors/esbuild diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..245be9d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +本文件记录项目的 notable 变更,格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。 + +## [Unreleased] + +### Added + +- Docker 多阶段构建与 Gitea CI 流水线 +- Vitest 单元测试(146 用例) +- 开源文档:LICENSE、CONTRIBUTING、SECURITY、NOTICE + +## [1.0.0] - 2026-06-24 + +### Added + +- JSON 格式化、验证与树形视图 +- 文本 / JSON 对比工具(行级、字符级 diff) +- Base64 / URL / Unicode / Zlib 编解码 +- 变量名格式转换(camelCase、snake_case 等) +- 二维码生成 +- 时间戳与时间字符串互转 +- 颜色格式转换(RGB / HEX / HSL) + +[Unreleased]: https://git.rc707blog.top/rose_cat707/ToolBox/compare/v1.0.0...HEAD +[1.0.0]: https://git.rc707blog.top/rose_cat707/ToolBox/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8ba1f0f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# 贡献指南 + +感谢你对 ToolBox 的关注!欢迎通过 Issue 或 Pull Request 参与贡献。 + +## 开始之前 + +1. 搜索 [Issues](https://git.rc707blog.top/rose_cat707/ToolBox/issues),确认是否已有相关讨论 +2. 较大改动请先开 Issue 说明方案,避免重复劳动 + +## 本地开发 + +```bash +git clone https://git.rc707blog.top/rose_cat707/ToolBox.git +cd ToolBox +npm install +npm run dev +``` + +## 代码规范 + +- 遵循项目现有风格,改动范围尽量小 +- 业务逻辑优先放在 `src/utils/`,便于单元测试 +- 不要提交 `node_modules`、`dist`、密钥或 `.env` 等敏感文件 + +## 测试 + +提交前请确保测试通过: + +```bash +npm run test:run +npm run build +``` + +新增功能或修复 bug 时,请补充或更新 `tests/` 下对应测试。 + +## 提交 Pull Request + +1. Fork 仓库并创建功能分支(如 `feat/xxx`、`fix/xxx`) +2. 编写清晰的 commit message,说明「为什么」而不只是「改了什么」 +3. 确保 CI 通过(npm test + Docker 构建) +4. 在 PR 描述中说明改动内容、测试方式 + +## 报告问题 + +- Bug:请提供复现步骤、期望行为与实际行为 +- 功能建议:说明使用场景与预期效果 +- 安全漏洞:请参见 [SECURITY.md](./SECURITY.md),勿在公开 Issue 中披露细节 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d90c91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,65 @@ +# Vue 静态站点多阶段构建:Node 构建 + Nginx 运行 +# CI 会通过 buildx 传入 IMAGE_PREFIX(见 .gitea/workflows/ci.yml) +# +# 构建时环境变量(可选): +# VITE_APP_TITLE 站点标题 +# VITE_APP_ICP 备案号 +# VITE_GA_MEASUREMENT_ID Google Analytics(留空禁用) +# VITE_JINRISHICI_SDK_URL 今日诗词 SDK(留空禁用) +# 运行时环境变量(可选,覆盖构建值): +# APP_TITLE 站点标题 +# APP_ICP 备案号(设为空字符串则不显示) +# APP_GA_MEASUREMENT_ID Google Analytics(设为空字符串则禁用) +# APP_JINRISHICI_SDK_URL 今日诗词 SDK(设为空字符串则禁用) + +ARG IMAGE_PREFIX= +ARG VITE_APP_TITLE=ToolBox +ARG VITE_APP_ICP= +ARG VITE_GA_MEASUREMENT_ID= +ARG VITE_JINRISHICI_SDK_URL= + +# ---------- 1) 前端构建 ---------- +FROM --platform=$BUILDPLATFORM ${IMAGE_PREFIX}node:20-alpine AS web-builder + +ARG VITE_APP_TITLE +ARG VITE_APP_ICP +ARG VITE_GA_MEASUREMENT_ID +ARG VITE_JINRISHICI_SDK_URL +ENV VITE_APP_TITLE=${VITE_APP_TITLE} +ENV VITE_APP_ICP=${VITE_APP_ICP} +ENV VITE_GA_MEASUREMENT_ID=${VITE_GA_MEASUREMENT_ID} +ENV VITE_JINRISHICI_SDK_URL=${VITE_JINRISHICI_SDK_URL} + +WORKDIR /src + +ENV npm_config_registry=https://registry.npmmirror.com \ + npm_config_esbuild_binary_host_mirror=https://npmmirror.com/mirrors/esbuild + +COPY package.json package-lock.json .npmrc ./ +RUN npm ci --userconfig=/src/.npmrc --registry=https://registry.npmmirror.com + +COPY index.html vite.config.js ./ +COPY public ./public +COPY src ./src + +RUN npm run build + +# ---------- 2) 运行镜像 ---------- +FROM ${IMAGE_PREFIX}nginx:1.27-alpine + +RUN apk add --no-cache tzdata \ + && rm -rf /var/cache/apk/* + +ENV TZ=Asia/Shanghai + +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf +COPY deploy/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh +COPY --from=web-builder /src/dist /usr/share/nginx/html + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 + +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f2bdb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 renjue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..6952247 --- /dev/null +++ b/NOTICE @@ -0,0 +1,19 @@ +ToolBox +Copyright (c) 2026 renjue + +This product includes software developed by third parties: + +- Vue.js — MIT License — https://github.com/vuejs/core +- Vue Router — MIT License — https://github.com/vuejs/router +- Vite — MIT License — https://github.com/vitejs/vite +- Vitest — MIT License — https://github.com/vitest-dev/vitest +- Font Awesome Free — Icons: CC BY 4.0, Fonts: SIL OFL 1.1 — https://fontawesome.com +- fflate — MIT License — https://github.com/101arrowz/fflate +- qrcode — MIT License — https://github.com/soldair/node-qrcode + +Runtime third-party services (loaded at runtime, not bundled): + +- Google Analytics — https://policies.google.com/privacy +- 今日诗词 SDK (jinrishici.com) — https://www.jinrishici.com/ + +See package licenses in node_modules for full dependency attribution. diff --git a/README.md b/README.md new file mode 100644 index 0000000..06084b2 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# ToolBox + +在线开发者工具箱:JSON 格式化、文本/JSON 对比、编解码、变量名转换、二维码、时间戳、颜色转换等。 + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE) + +## 功能 + +| 工具 | 路径 | 说明 | +|------|------|------| +| JSON | `/json-formatter` | 格式化、压缩、转义、JSONPath 筛选、树形视图 | +| 对比 | `/comparator` | 文本行/字符 diff、JSON 结构对比 | +| 编解码 | `/encoder-decoder` | Base64、URL、Unicode、Zlib | +| 变量名 | `/variable-name` | camelCase / PascalCase / snake_case 等 | +| 二维码 | `/qr-code` | 文本生成二维码 | +| 时间戳 | `/timestamp-converter` | 秒/毫秒/纳秒与时间字符串互转 | +| 颜色 | `/color-converter` | RGB / HEX / HSL 互转 | + +## 快速开始 + +### 前置要求 + +- Node.js >= 18 +- npm + +### 安装与运行 + +```bash +git clone https://git.rc707blog.top/rose_cat707/ToolBox.git +cd ToolBox +npm install +npm run dev +``` + +开发服务器默认 `http://localhost:3000`。 + +### 构建 + +```bash +npm run build # 产物输出到 dist/ +npm run preview # 预览生产构建 +``` + +### 测试 + +```bash +npm run test:run +``` + +## 环境变量 + +站点标题与备案号可通过环境变量配置,复制 `.env.example` 为 `.env` 后修改: + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `VITE_APP_TITLE` | 站点标题(导航栏、浏览器标签) | `ToolBox` | +| `VITE_APP_ICP` | 备案号(留空则不显示) | (空) | +| `VITE_GA_MEASUREMENT_ID` | Google Analytics 测量 ID(留空禁用) | (空,不上报) | +| `VITE_JINRISHICI_SDK_URL` | 今日诗词 SDK 地址(留空禁用) | (空,不加载) | + +### Docker + +**构建时**指定(写入静态资源默认值): + +```bash +docker build \ + --build-arg VITE_APP_TITLE="我的工具箱" \ + --build-arg VITE_GA_MEASUREMENT_ID="G-XXXXXXXXXX" \ + -t toolbox:latest . +``` + +**运行时**覆盖(无需重新构建,容器启动时生成 `config.js`): + +```bash +docker run --rm -p 8080:80 \ + -e APP_TITLE="我的工具箱" \ + -e APP_ICP="" \ + -e APP_GA_MEASUREMENT_ID="" \ + toolbox:latest +``` + +`APP_ICP` 设为空字符串时,首页不显示备案号。 + +## Docker 部署 + +```bash +docker build -t toolbox:latest . +docker run --rm -p 8080:80 toolbox:latest +``` + +访问 `http://localhost:8080`。 + +CI 会在 push 到 main/master 时自动构建并推送镜像到 Gitea Container Registry,详见 [`.gitea/README.md`](./.gitea/README.md)。 + +## 项目结构 + +``` +ToolBox/ +├── src/ +│ ├── views/ # 页面组件 +│ ├── utils/ # 可测试的业务逻辑 +│ ├── composables/ # Vue composables +│ ├── components/ # 公共组件 +│ └── router/ # 路由 +├── tests/ # Vitest 单元测试 +├── deploy/ # Nginx 配置 +├── Dockerfile +└── .gitea/workflows/ # CI 流水线 +``` + +## 添加新工具 + +1. 在 `src/views/` 创建 Vue 组件 +2. 在 `src/router/index.js` 添加路由 +3. 在 `src/App.vue` 导航栏添加入口 +4. 在 `src/views/Home.vue` 的 `tools` 数组中注册 +5. 如有纯逻辑,提取到 `src/utils/` 并补充测试 + +## 自部署说明 + +- **Google Analytics**:通过 `VITE_GA_MEASUREMENT_ID` / `APP_GA_MEASUREMENT_ID` 启用;默认关闭,不向 Google 上报数据 +- **今日诗词**:通过 `VITE_JINRISHICI_SDK_URL` / `APP_JINRISHICI_SDK_URL` 启用;默认关闭,不加载第三方 SDK +- **deploy.sh**:作者自用 SSH 部署脚本,内含固定远程路径,使用前请修改 `REMOTE_DIR` + +## 第三方依赖 + +主要依赖及许可证见 [NOTICE](./NOTICE)。Font Awesome 图标需保留其版权声明。 + +## 贡献 + +欢迎提交 Issue 和 Pull Request,请参阅 [CONTRIBUTING.md](./CONTRIBUTING.md)。 + +## 安全 + +发现安全漏洞请私下报告,详见 [SECURITY.md](./SECURITY.md)。 + +## 许可证 + +[MIT License](./LICENSE) © 2026 renjue diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5048e6f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ +# 安全策略 + +## 支持的版本 + +| 版本 | 支持状态 | +|--------|----------| +| 最新版 | ✅ | +| 旧版本 | ❌ | + +## 报告漏洞 + +如果你发现了安全漏洞,**请勿在公开 Issue 中披露**。 + +请通过以下方式私下报告: + +- 在 [Gitea Issues](https://git.rc707blog.top/rose_cat707/ToolBox/issues) 提交说明(标题注明 `[SECURITY]`,勿公开漏洞细节) +- 或在 Gitea 仓库中使用 **Private Security Advisory**(若已启用) + +报告时请尽量包含: + +- 漏洞类型与影响范围 +- 复现步骤 +- 受影响版本 +- 可能的修复建议(如有) + +我们会在合理时间内确认收到,并在修复后公开致谢(除非你希望匿名)。 + +## 安全注意事项(自部署) + +- 本项目为纯前端静态应用,Docker 镜像使用 Nginx 提供静态文件 +- **Google Analytics** 与 **今日诗词 SDK** 默认关闭,需通过环境变量显式启用(见 README) +- `deploy.sh` 通过命令行参数传递 SSH 密码,请勿在脚本或日志中硬编码凭据 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..a53561c --- /dev/null +++ b/deploy.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -e + +# 部署脚本:打包并通过 SSH 将构建产物同步到远程目录 +# 用法: ./deploy.sh <服务器IP> <登录账户> <登录密码> [sudo] +# 示例: ./deploy.sh 192.168.1.100 22 root mypassword +# 若远程目录无写权限,加第5个参数 sudo:先传到 /tmp,再通过 sudo 拷到目标(会用到同一密码作为 sudo 密码) +# 示例: ./deploy.sh 192.168.1.100 22 myuser mypassword sudo + +if [ $# -lt 4 ]; then + echo "用法: $0 <服务器IP> <登录账户> <登录密码> [sudo]" + echo "示例: $0 192.168.1.100 22 root mypassword" + echo "远程目录无写权限时加第5参数: $0 <端口> <用户> <密码> sudo" + exit 1 +fi + +HOST="$1" +PORT="$2" +USER="$3" +PASSWORD="$4" +USE_SUDO="${5:-}" +REMOTE_DIR="/root/caddy/site/tool" +REMOTE_TMP="/tmp/tool_deploy_$$" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIST_DIR="${SCRIPT_DIR}/dist" +SSH_OPTS="-p ${PORT} -o StrictHostKeyChecking=accept-new -T" + +echo ">>> 正在打包..." +cd "$SCRIPT_DIR" +npm run build + +if [ ! -d "$DIST_DIR" ]; then + echo "错误: 构建产物目录 dist 不存在" + exit 1 +fi + +# 检查 sshpass 是否可用(用于非交互式传入密码) +if ! command -v sshpass &> /dev/null; then + echo "未找到 sshpass,请先安装:" + echo " macOS: brew install sshpass" + echo " Ubuntu: sudo apt-get install sshpass" + exit 1 +fi + +export SSHPASS="$PASSWORD" + +if [ "$USE_SUDO" = "sudo" ]; then + echo ">>> 正在同步到远程临时目录 ${REMOTE_TMP}" + sshpass -e rsync -avz --delete \ + -e "ssh ${SSH_OPTS}" \ + "${DIST_DIR}/" \ + "${USER}@${HOST}:${REMOTE_TMP}/" + echo ">>> 正在用 sudo 拷贝到目标目录 ${REMOTE_DIR}" + # 只执行一次 sudo(读一次密码),在子 shell 里完成 rsync 与清理,避免第二次 sudo 无密码 + printf '%s\n' "$PASSWORD" | sshpass -e ssh ${SSH_OPTS} "${USER}@${HOST}" \ + "sudo -S sh -c 'rsync -avz --delete ${REMOTE_TMP}/ ${REMOTE_DIR}/ && rm -rf ${REMOTE_TMP}'" +else + echo ">>> 正在通过 SSH 同步到 ${USER}@${HOST}:${PORT} -> ${REMOTE_DIR}" + sshpass -e rsync -avz --delete \ + -e "ssh ${SSH_OPTS}" \ + "${DIST_DIR}/" \ + "${USER}@${HOST}:${REMOTE_DIR}/" +fi + +unset SSHPASS +echo ">>> 部署完成" diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh new file mode 100755 index 0000000..f383df0 --- /dev/null +++ b/deploy/docker-entrypoint.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -e + +json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r//g' +} + +if [ -z "${APP_TITLE+x}" ]; then + APP_TITLE="ToolBox" +fi +if [ -z "${APP_ICP+x}" ]; then + APP_ICP="" +fi + +TITLE_ESC=$(json_escape "$APP_TITLE") +ICP_ESC=$(json_escape "$APP_ICP") + +CONFIG_BODY="title: \"${TITLE_ESC}\", icp: \"${ICP_ESC}\"" + +if [ -n "${APP_GA_MEASUREMENT_ID+x}" ]; then + GA_ESC=$(json_escape "$APP_GA_MEASUREMENT_ID") + CONFIG_BODY="${CONFIG_BODY}, gaId: \"${GA_ESC}\"" +fi + +if [ -n "${APP_JINRISHICI_SDK_URL+x}" ]; then + JRS_ESC=$(json_escape "$APP_JINRISHICI_SDK_URL") + CONFIG_BODY="${CONFIG_BODY}, jinrishiciSdkUrl: \"${JRS_ESC}\"" +fi + +cat > /usr/share/nginx/html/config.js < + + + + + %VITE_APP_TITLE% + + + +
+ + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..931f168 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3945 @@ +{ + "name": "toolbox", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "toolbox", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-free": "^7.1.0", + "fflate": "^0.8.2", + "qrcode": "^1.5.4", + "vue": "^3.4.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "@vitest/coverage-v8": "^4.1.9", + "jsdom": "^29.1.1", + "vite": "^5.0.0", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", + "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "vue": "3.5.38" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.4" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.38", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7344b0a --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "toolbox", + "version": "1.0.0", + "description": "A Vue-based online toolbox: JSON, diff, encode/decode, and more", + "license": "MIT", + "type": "module", + "author": "rc_707@outlook.com", + "repository": { + "type": "git", + "url": "https://git.rc707blog.top/rose_cat707/ToolBox.git" + }, + "homepage": "https://git.rc707blog.top/rose_cat707/ToolBox", + "bugs": { + "url": "https://git.rc707blog.top/rose_cat707/ToolBox/issues" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^7.1.0", + "fflate": "^0.8.2", + "qrcode": "^1.5.4", + "vue": "^3.4.0", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "@vitest/coverage-v8": "^4.1.9", + "jsdom": "^29.1.1", + "vite": "^5.0.0", + "vitest": "^4.1.9" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/public/config.js b/public/config.js new file mode 100644 index 0000000..6ed0dad --- /dev/null +++ b/public/config.js @@ -0,0 +1 @@ +window.__SITE_CONFIG__ = window.__SITE_CONFIG__ || {}; diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..34fdac3 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,146 @@ + + + + + + diff --git a/src/components/DateTimePicker.vue b/src/components/DateTimePicker.vue new file mode 100644 index 0000000..c18bb91 --- /dev/null +++ b/src/components/DateTimePicker.vue @@ -0,0 +1,818 @@ + + + + + diff --git a/src/components/JsonTreeNode.vue b/src/components/JsonTreeNode.vue new file mode 100644 index 0000000..029e5b8 --- /dev/null +++ b/src/components/JsonTreeNode.vue @@ -0,0 +1,396 @@ + + + + + diff --git a/src/composables/useLineNumberEditor.js b/src/composables/useLineNumberEditor.js new file mode 100644 index 0000000..66cd7d7 --- /dev/null +++ b/src/composables/useLineNumberEditor.js @@ -0,0 +1,51 @@ +import { ref, nextTick } from 'vue' + +const LINE_HEIGHT = 22.4 +const PADDING = 16 + +export function countLines(text) { + return text ? text.split('\n').length : 1 +} + +export function useLineNumberEditor(getText, options = {}) { + const { onBeforeUpdate } = options + + const containerRef = ref(null) + const editorRef = ref(null) + const lineCount = ref(1) + + const adjustEditorHeight = () => { + const editor = editorRef.value + if (!editor) return + + editor.style.height = 'auto' + const scrollHeight = editor.scrollHeight + const minHeight = PADDING + LINE_HEIGHT + PADDING + editor.style.height = `${Math.max(scrollHeight, minHeight)}px` + } + + const updateLineCount = () => { + if (onBeforeUpdate) onBeforeUpdate() + lineCount.value = countLines(getText()) + nextTick(adjustEditorHeight) + } + + const resetEditorScroll = () => { + if (containerRef.value) containerRef.value.scrollTop = 0 + if (editorRef.value) editorRef.value.scrollTop = 0 + } + + const initEditor = () => { + nextTick(updateLineCount) + } + + return { + containerRef, + editorRef, + lineCount, + updateLineCount, + adjustEditorHeight, + resetEditorScroll, + initEditor, + } +} diff --git a/src/config/site.js b/src/config/site.js new file mode 100644 index 0000000..037ce4d --- /dev/null +++ b/src/config/site.js @@ -0,0 +1,62 @@ +const DEFAULTS = { + title: 'ToolBox', + icp: '', + gaId: '', + jinrishiciSdkUrl: '', +} + +function getRuntimeConfig() { + if (typeof window !== 'undefined' && window.__SITE_CONFIG__) { + return window.__SITE_CONFIG__ + } + return {} +} + +function getConfigValue(runtimeKey, envKey, defaultValue) { + const runtime = getRuntimeConfig() + if (Object.prototype.hasOwnProperty.call(runtime, runtimeKey)) { + return runtime[runtimeKey] + } + const fromEnv = import.meta.env[envKey] + if (fromEnv != null) { + return fromEnv + } + return defaultValue +} + +export function getSiteTitle() { + const value = getConfigValue('title', 'VITE_APP_TITLE', DEFAULTS.title) + return value || DEFAULTS.title +} + +export function getSiteIcp() { + return getConfigValue('icp', 'VITE_APP_ICP', DEFAULTS.icp) +} + +export function getGaMeasurementId() { + return getConfigValue('gaId', 'VITE_GA_MEASUREMENT_ID', DEFAULTS.gaId) +} + +export function getJinrishiciSdkUrl() { + return getConfigValue('jinrishiciSdkUrl', 'VITE_JINRISHICI_SDK_URL', DEFAULTS.jinrishiciSdkUrl) +} + +export function getPageTitle(suffix) { + const base = getSiteTitle() + return suffix ? `${base}-${suffix}` : base +} + +export const siteConfig = { + get title() { + return getSiteTitle() + }, + get icp() { + return getSiteIcp() + }, + get gaMeasurementId() { + return getGaMeasurementId() + }, + get jinrishiciSdkUrl() { + return getJinrishiciSdkUrl() + }, +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..cef4d24 --- /dev/null +++ b/src/main.js @@ -0,0 +1,12 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import { initAnalytics } from './utils/analytics.js' +import './style.css' +// 引入 Font Awesome 6.4 +import '@fortawesome/fontawesome-free/css/all.css' + +initAnalytics() + +createApp(App).use(router).mount('#app') + diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..9149ad7 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,82 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Home from '../views/Home.vue' +import { getPageTitle } from '../config/site.js' + +const routes = [ + { + path: '/', + name: 'Home', + component: Home, + meta: { + titleSuffix: null + } + }, + { + path: '/json-formatter', + name: 'JsonFormatter', + component: () => import('../views/JsonFormatter.vue'), + meta: { + titleSuffix: 'JSON' + } + }, + { + path: '/comparator', + name: 'Comparator', + component: () => import('../views/Comparator.vue'), + meta: { + titleSuffix: '对比' + } + }, + { + path: '/encoder-decoder', + name: 'EncoderDecoder', + component: () => import('../views/EncoderDecoder.vue'), + meta: { + titleSuffix: '编解码' + } + }, + { + path: '/variable-name', + name: 'VariableNameConverter', + component: () => import('../views/VariableNameConverter.vue'), + meta: { + titleSuffix: '变量名' + } + }, + { + path: '/qr-code', + name: 'QRCodeGenerator', + component: () => import('../views/QRCodeGenerator.vue'), + meta: { + titleSuffix: '二维码' + } + }, + { + path: '/timestamp-converter', + name: 'TimestampConverter', + component: () => import('../views/TimestampConverter.vue'), + meta: { + titleSuffix: '时间戳' + } + }, + { + path: '/color-converter', + name: 'ColorConverter', + component: () => import('../views/ColorConverter.vue'), + meta: { + titleSuffix: '颜色' + } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach((to, from, next) => { + document.title = getPageTitle(to.meta.titleSuffix) + next() +}) + +export default router diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..35bdfef --- /dev/null +++ b/src/style.css @@ -0,0 +1,19 @@ +@import './styles/line-number-editor.css'; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: #ffffff; + min-height: 100vh; + color: #000000; +} + +#app { + min-height: 100vh; +} + diff --git a/src/styles/line-number-editor.css b/src/styles/line-number-editor.css new file mode 100644 index 0000000..2350f7d --- /dev/null +++ b/src/styles/line-number-editor.css @@ -0,0 +1,44 @@ +.editor-container { + flex: 1; + display: block; + position: relative; + overflow-y: auto; + overflow-x: hidden; + background: #ffffff; + min-height: 0; +} + +.editor-body { + display: flex; + align-items: flex-start; + min-height: 100%; +} + +.editor-body > .line-numbers { + flex-shrink: 0; + width: 40px; + padding: 1rem 0.5rem; + background: #fafafa; + border-right: 1px solid #e5e5e5; + font-family: 'Courier New', monospace; + font-size: 14px; + color: #999999; + text-align: right; + user-select: none; + box-sizing: border-box; +} + +.editor-body > .line-numbers .line-number { + line-height: 1.6; + height: 22.4px; + display: flex; + align-items: center; + justify-content: flex-end; +} + +@media (max-width: 768px) { + .editor-body > .line-numbers { + width: 32px; + font-size: 12px; + } +} diff --git a/src/utils/analytics.js b/src/utils/analytics.js new file mode 100644 index 0000000..c6b5f8e --- /dev/null +++ b/src/utils/analytics.js @@ -0,0 +1,18 @@ +import { getGaMeasurementId } from '../config/site.js' + +export function initAnalytics() { + const measurementId = getGaMeasurementId() + if (!measurementId) return + + window.dataLayer = window.dataLayer || [] + window.gtag = function gtag() { + window.dataLayer.push(arguments) + } + window.gtag('js', new Date()) + window.gtag('config', measurementId) + + const script = document.createElement('script') + script.async = true + script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}` + document.head.appendChild(script) +} diff --git a/src/utils/byteUtils.js b/src/utils/byteUtils.js new file mode 100644 index 0000000..527dc70 --- /dev/null +++ b/src/utils/byteUtils.js @@ -0,0 +1,10 @@ +export function getByteLength(str) { + return new TextEncoder().encode(str).length +} + +export function truncateToMaxBytes(str, maxBytes) { + if (getByteLength(str) <= maxBytes) return str + let end = str.length + while (end > 0 && getByteLength(str.slice(0, end)) > maxBytes) end-- + return str.slice(0, end) +} diff --git a/src/utils/color.js b/src/utils/color.js new file mode 100644 index 0000000..81aa6fc --- /dev/null +++ b/src/utils/color.js @@ -0,0 +1,87 @@ +export function rgbToHex(r, g, b) { + const toHex = (n) => { + const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16) + return hex.length === 1 ? '0' + hex : hex + } + return (toHex(r) + toHex(g) + toHex(b)).toUpperCase() +} + +export function rgbToHsl(r, g, b) { + r /= 255 + g /= 255 + b /= 255 + + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + let h, s, l = (max + min) / 2 + + if (max === min) { + h = s = 0 + } else { + const d = max - min + s = l > 0.5 ? d / (2 - max - min) : d / (max + min) + + switch (max) { + case r: + h = ((g - b) / d + (g < b ? 6 : 0)) / 6 + break + case g: + h = ((b - r) / d + 2) / 6 + break + case b: + h = ((r - g) / d + 4) / 6 + break + } + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + l: Math.round(l * 100) + } +} + +export function hexToRgb(hex) { + const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } + : null +} + +export function hslToRgb(h, s, l) { + h /= 360 + s /= 100 + l /= 100 + + let r, g, b + + if (s === 0) { + r = g = b = l + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1 / 6) return p + (q - p) * 6 * t + if (t < 1 / 2) return q + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 + return p + } + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s + const p = 2 * l - q + + r = hue2rgb(p, q, h + 1 / 3) + g = hue2rgb(p, q, h) + b = hue2rgb(p, q, h - 1 / 3) + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + } +} diff --git a/src/utils/comparator/html.js b/src/utils/comparator/html.js new file mode 100644 index 0000000..3c22a3a --- /dev/null +++ b/src/utils/comparator/html.js @@ -0,0 +1,31 @@ +export function escapeHtml(text) { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +export function highlightDiff(text, diffRanges) { + if (!diffRanges || diffRanges.length === 0) { + return escapeHtml(text) + } + + let result = '' + let lastIndex = 0 + + diffRanges.forEach(range => { + if (range.start > lastIndex) { + result += escapeHtml(text.substring(lastIndex, range.start)) + } + result += `${escapeHtml(text.substring(range.start, range.end))}` + lastIndex = range.end + }) + + if (lastIndex < text.length) { + result += escapeHtml(text.substring(lastIndex)) + } + + return result +} diff --git a/src/utils/comparator/index.js b/src/utils/comparator/index.js new file mode 100644 index 0000000..c347e10 --- /dev/null +++ b/src/utils/comparator/index.js @@ -0,0 +1,3 @@ +export * from './html.js' +export * from './textDiff.js' +export * from './jsonCompare.js' diff --git a/src/utils/comparator/jsonCompare.js b/src/utils/comparator/jsonCompare.js new file mode 100644 index 0000000..e5e0a9b --- /dev/null +++ b/src/utils/comparator/jsonCompare.js @@ -0,0 +1,771 @@ +export function isNullOrUndefined(value) { + return value === undefined || value === null +} + +export function isNotNullAndUndefined(value) { + return value !== undefined && value !== null +} + +// 判断是否为基本类型(除字符串外) +export function isPrimitiveTypeOrNon(value) { + return isNullOrUndefined(value) || + typeof value === 'number' || + typeof value === 'boolean' || + (typeof value === 'string' && false) +} + +export function isPrimitiveType(value) { + return isNotNullAndUndefined(value) && ( + typeof value === 'number' || + typeof value === 'boolean' || + (typeof value === 'string' && false) + ) +} + + +// 判断是否为字符串 +export function isStringTypeOrNon(value) { + return isNullOrUndefined(value) || typeof value === 'string' +} + +export function isStringType(value) { + return isNotNullAndUndefined(value) && typeof value === 'string' +} + +// 判断是否为map +export function isMapTypeOrNon(value) { + return isNullOrUndefined(value) || (typeof value === 'object' && !Array.isArray(value)) +} + +export function isMapType(value) { + return isNotNullAndUndefined(value) && typeof value === 'object' && !Array.isArray(value) +} + +// 判断是否为list +export function isListTypeOrNon(value) { + return isNullOrUndefined(value) || Array.isArray(value) +} + +export function isListType(value) { + return isNotNullAndUndefined(value) && Array.isArray(value) +} + +// 判断是否为叶子节点(基本类型或字符串) +export function isLeafNodeOrNon(value) { + return isNullOrUndefined(value) || + typeof value === 'number' || + typeof value === 'boolean' || + typeof value === 'string' +} + +// 计算字符串的最长公共子串长度 +export function longestCommonSubstring(strA, strB) { + if (!strA || !strB) return 0 + + const m = strA.length + const n = strB.length + let maxLen = 0 + const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)) + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (strA[i - 1] === strB[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + maxLen = Math.max(maxLen, dp[i][j]) + } else { + dp[i][j] = 0 + } + } + } + + return maxLen +} + +// 计算字符串相似度(公共子串占全体的比例) +export function stringSimilarity(strA, strB) { + if (strA === strB) return {type: 'same', similarity: 1.0} + + const maxLen = Math.max(strA.length, strB.length) + if (maxLen === 0) return {type: 'same', similarity: 1.0} + + const lcsLen = longestCommonSubstring(strA, strB) + if (lcsLen === 0) { + return {type: 'different', similarity: 0.0} + } + + const similarity = lcsLen / maxLen + return {type: 'similar', similarity} +} + +// 节点对比结果类型 +export const NodeComparisonResult = { + SAME: 'same', // 相同 + SIMILAR: 'similar', // 相似 + DIFFERENT: 'different' // 不同 +} + +// 对比两个JSON节点,返回对比结果和相似度 +export function compareJsonNodes(nodeA, nodeB, ignoreOrder = false) { + const result = (type, similarity) => ({ type, similarity, nodeA, nodeB }) + + if (isNullOrUndefined(nodeA) && isNullOrUndefined(nodeB)) { + return nodeA === nodeB ? result(NodeComparisonResult.SAME, 1.0) : result(NodeComparisonResult.DIFFERENT, 0.0) + } + if (isNullOrUndefined(nodeA) || isNullOrUndefined(nodeB)) { + return result(NodeComparisonResult.DIFFERENT, 0.0) + } + if (isPrimitiveType(nodeA) && isPrimitiveType(nodeB)) { + return nodeA === nodeB ? result(NodeComparisonResult.SAME, 1.0) : result(NodeComparisonResult.DIFFERENT, 0.0) + } + if (isStringType(nodeA) && isStringType(nodeB)) { + const { type, similarity } = stringSimilarity(nodeA, nodeB) + return result(type, similarity) + } + if (typeof nodeA !== typeof nodeB) { + return result(NodeComparisonResult.DIFFERENT, 0.0) + } + if (isMapType(nodeA) && isMapType(nodeB)) return compareMaps(nodeA, nodeB, ignoreOrder) + if (isListType(nodeA) && isListType(nodeB)) { + // 根据参数选择是否忽略列表顺序 + return ignoreOrder ? compareListsIgnoreOrder(nodeA, nodeB, ignoreOrder) : compareLists(nodeA, nodeB, ignoreOrder) + } + return result(NodeComparisonResult.DIFFERENT, 0.0) +} + +// 对比Map(对象) +export function compareMaps(mapA, mapB, ignoreOrder = false) { + const keysA = Object.keys(mapA).sort() + const keysB = Object.keys(mapB).sort() + const setA = new Set(keysA) + const setB = new Set(keysB) + const allKeys = new Set([...keysA, ...keysB]) + const comparisons = [] + let allSame = true + let allDifferent = true + let allInsert = true + let allDelete = true + + for (const key of allKeys) { + const hasKeyA = setA.has(key) + const hasKeyB = setB.has(key) + const valueComparison = compareJsonNodes( + hasKeyA ? mapA[key] : undefined, + hasKeyB ? mapB[key] : undefined, + ignoreOrder + ) + if (hasKeyA && hasKeyB && valueComparison.type === NodeComparisonResult.DIFFERENT) { + valueComparison.type = NodeComparisonResult.SIMILAR + valueComparison.similarity = 0.5 + } + if (valueComparison.type === NodeComparisonResult.SAME) { + allDifferent = false + } else if (valueComparison.type === NodeComparisonResult.SIMILAR) { + allSame = false + allDifferent = false + allInsert = false + allDelete = false + } else if (valueComparison.type === NodeComparisonResult.DIFFERENT) { + allSame = false + if (isNullOrUndefined(mapA) || hasKeyA) allInsert = false + if (isNullOrUndefined(mapB) || hasKeyB) allDelete = false + } + comparisons.push({ ...valueComparison, key }) + } + + const base = { nodeA: mapA, nodeB: mapB, children: comparisons } + if (allSame) { + return { type: NodeComparisonResult.SAME, similarity: 1.0, ...base } + } + if (allDifferent && !allInsert && !allDelete) { + return { type: NodeComparisonResult.DIFFERENT, similarity: 0.0, ...base } + } + const avgSimilarity = comparisons.length > 0 + ? comparisons.reduce((sum, c) => sum + c.similarity, 0) / comparisons.length + : 0 + return { type: NodeComparisonResult.SIMILAR, similarity: avgSimilarity, ...base } +} + +// 对比List(数组)- 忽略顺序,使用贪心算法寻找最佳匹配 +export function compareListsIgnoreOrder(listA, listB, ignoreOrder = false) { + const n = listA.length + const m = listB.length + + // 计算所有元素对的相似度矩阵 + const similarityMatrix = [] + for (let i = 0; i < n; i++) { + similarityMatrix[i] = [] + for (let j = 0; j < m; j++) { + const comp = compareJsonNodes(listA[i], listB[j], ignoreOrder) + similarityMatrix[i][j] = comp + } + } + + // 使用贪心算法找到最佳匹配(不考虑顺序) + // 每次选择相似度最高的未匹配对 + const matches = [] + const usedA = new Set() + const usedB = new Set() + + // 创建所有可能的匹配对,按相似度降序排序 + const allPairs = [] + for (let i = 0; i < n; i++) { + for (let j = 0; j < m; j++) { + allPairs.push({ + indexA: i, + indexB: j, + similarity: similarityMatrix[i][j].similarity, + comparison: similarityMatrix[i][j] + }) + } + } + allPairs.sort((a, b) => b.similarity - a.similarity) + + // 贪心匹配:选择相似度最高的未匹配对 + for (const pair of allPairs) { + if (!usedA.has(pair.indexA) && !usedB.has(pair.indexB)) { + matches.push({ + indexA: pair.indexA, + indexB: pair.indexB, + comparison: pair.comparison + }) + usedA.add(pair.indexA) + usedB.add(pair.indexB) + } + } + + // 添加未匹配的A中元素(删除) + for (let i = 0; i < n; i++) { + if (!usedA.has(i)) { + matches.push({ + indexA: i, + indexB: undefined, + comparison: { + type: NodeComparisonResult.DIFFERENT, + similarity: 0.0, + nodeA: listA[i], + nodeB: undefined + } + }) + } + } + + // 添加未匹配的B中元素(插入) + for (let j = 0; j < m; j++) { + if (!usedB.has(j)) { + matches.push({ + indexA: undefined, + indexB: j, + comparison: { + type: NodeComparisonResult.DIFFERENT, + similarity: 0.0, + nodeA: undefined, + nodeB: listB[j] + } + }) + } + } + + // 按原始顺序排序匹配结果(先A后B,保持展示的一致性) + matches.sort((a, b) => { + if (a.indexA !== undefined && b.indexA !== undefined) { + return a.indexA - b.indexA + } + if (a.indexA !== undefined) return -1 + if (b.indexA !== undefined) return 1 + if (a.indexB !== undefined && b.indexB !== undefined) { + return a.indexB - b.indexB + } + return 0 + }) + + // 判断父节点类型 + const totalElements = n + m + const matchedElements = matches.filter(m => m.indexA !== undefined && m.indexB !== undefined).length + const sameMatches = matches.filter(m => + m.indexA !== undefined && + m.indexB !== undefined && + m.comparison.type === NodeComparisonResult.SAME + ).length + const differentMatches = matches.filter(m => + m.comparison.type === NodeComparisonResult.DIFFERENT + ).length + + if (totalElements === 0) { + return { + type: NodeComparisonResult.SAME, + similarity: 1.0, + nodeA: listA, + nodeB: listB, + matches + } + } + + if (n !== 0 && m !== 0 && differentMatches === totalElements) { + return { + type: NodeComparisonResult.DIFFERENT, + similarity: 0.0, + nodeA: listA, + nodeB: listB, + matches + } + } + + if (sameMatches === matchedElements && matchedElements === totalElements) { + return { + type: NodeComparisonResult.SAME, + similarity: 1.0, + nodeA: listA, + nodeB: listB, + matches + } + } + + // 计算加权平均相似度 + const totalSimilarity = matches.reduce((sum, m) => sum + m.comparison.similarity, 0) + const avgSimilarity = totalElements > 0 ? totalSimilarity / totalElements : 0 + + return { + type: NodeComparisonResult.SIMILAR, + similarity: avgSimilarity, + nodeA: listA, + nodeB: listB, + matches + } +} + +// 对比List(数组)- 寻找最佳匹配使相似度最高 +export function compareLists(listA, listB, ignoreOrder = false) { + const n = listA.length + const m = listB.length + + // 计算所有元素对的相似度矩阵 + const similarityMatrix = [] + for (let i = 0; i < n; i++) { + similarityMatrix[i] = [] + for (let j = 0; j < m; j++) { + const comp = compareJsonNodes(listA[i], listB[j], ignoreOrder) + similarityMatrix[i][j] = comp + } + } + + // 使用动态规划寻找最佳匹配(最大化总相似度) + // dp[i][j] 表示 listA[0..i-1] 和 listB[0..j-1] 的最佳匹配总相似度 + const dp = Array(n + 1).fill(null).map(() => Array(m + 1).fill(0)) + const path = Array(n + 1).fill(null).map(() => Array(m + 1).fill(null)) + + // 初始化边界情况:当j=0时,只能跳过A中的元素 + for (let i = 1; i <= n; i++) { + path[i][0] = 'skipA' + } + // 初始化边界情况:当i=0时,只能跳过B中的元素 + for (let j = 1; j <= m; j++) { + path[0][j] = 'skipB' + } + + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + const matchSimilarity = similarityMatrix[i - 1][j - 1].similarity + const matchScore = dp[i - 1][j - 1] + matchSimilarity + const skipAScore = dp[i - 1][j] + const skipBScore = dp[i][j - 1] + + if (matchScore >= skipAScore && matchScore >= skipBScore) { + dp[i][j] = matchScore + path[i][j] = 'match' + } else if (skipAScore >= skipBScore) { + dp[i][j] = skipAScore + path[i][j] = 'skipA' + } else { + dp[i][j] = skipBScore + path[i][j] = 'skipB' + } + } + } + + // 回溯构建匹配结果 + const matches = [] + let i = n, j = m + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && path[i][j] === 'match') { + matches.unshift({ + indexA: i - 1, + indexB: j - 1, + comparison: similarityMatrix[i - 1][j - 1] + }) + i-- + j-- + } else if (i > 0 && path[i][j] === 'skipA') { + matches.unshift({ + indexA: i - 1, + indexB: undefined, + comparison: { + type: NodeComparisonResult.DIFFERENT, + similarity: 0.0, + nodeA: listA[i - 1], + nodeB: undefined + } + }) + i-- + } else if (j > 0) { + matches.unshift({ + indexA: undefined, + indexB: j - 1, + comparison: { + type: NodeComparisonResult.DIFFERENT, + similarity: 0.0, + nodeA: undefined, + nodeB: listB[j - 1] + } + }) + j-- + } else { + break + } + } + + // 判断父节点类型 + const totalElements = n + m + const matchedElements = matches.filter(m => m.indexA !== null && m.indexB !== null).length + const sameMatches = matches.filter(m => + m.indexA !== undefined && + m.indexB !== undefined && + m.comparison.type === NodeComparisonResult.SAME + ).length + const differentMatches = matches.filter(m => + m.comparison.type === NodeComparisonResult.DIFFERENT + ).length + + if (totalElements === 0) { + return { + type: NodeComparisonResult.SAME, + similarity: 1.0, + nodeA: listA, + nodeB: listB, + matches + } + } + + if (n !== 0 && m !== 0 && differentMatches === totalElements) { + return { + type: NodeComparisonResult.DIFFERENT, + similarity: 0.0, + nodeA: listA, + nodeB: listB, + matches + } + } + + if (sameMatches === matchedElements && matchedElements === totalElements) { + return { + type: NodeComparisonResult.SAME, + similarity: 1.0, + nodeA: listA, + nodeB: listB, + matches + } + } + + // 计算加权平均相似度 + const totalSimilarity = matches.reduce((sum, m) => sum + m.comparison.similarity, 0) + const avgSimilarity = totalElements > 0 ? totalSimilarity / totalElements : 0 + + return { + type: NodeComparisonResult.SIMILAR, + similarity: avgSimilarity, + nodeA: listA, + nodeB: listB, + matches + } +} + +// 在展示行末尾添加/移除逗号的辅助 +export function addTrailingComma(line, lastNotEmptyRef) { + if (line.content !== '') { + line.content += ',' + lastNotEmptyRef.current = line + } +} +export function removeTrailingComma(lastNotEmptyRef) { + if (lastNotEmptyRef.current !== undefined) { + lastNotEmptyRef.current.content = lastNotEmptyRef.current.content.slice(0, -1) + } +} + +// 将对比结果转换为展示格式 +export function convertComparisonToDisplay(comparison, indent = 0) { + const indentStr = ' '.repeat(indent) + const childIndentStr = ' '.repeat(indent + 1) + const isLeaf = !comparison.children && !comparison.matches + + // 根据展示规则确定类型 + const { type, nodeA, nodeB } = comparison + let displayType = 'same' + if (type === NodeComparisonResult.DIFFERENT) { + if (nodeA !== undefined && nodeB === undefined) displayType = 'delete' + else if (nodeA === undefined && nodeB !== undefined) displayType = 'insert' + else displayType = 'different' + } else if (type === NodeComparisonResult.SIMILAR && isLeaf) { + displayType = 'modify' + } + + // 处理 Map(对象) + if (!isLeaf && isMapType(nodeA) && isMapType(nodeB)) { + const leftLines = [] + const rightLines = [] + const sortedKeys = [...new Set(comparison.children.map(c => c.key).filter(Boolean))].sort() + const commaLeft = { current: undefined } + const commaRight = { current: undefined } + + leftLines.push({ type: displayType, content: indentStr + '{' }) + rightLines.push({ type: displayType, content: indentStr + '{' }) + + for (const key of sortedKeys) { + const child = comparison.children.find(c => c.key === key) + const childResult = convertComparisonToDisplay(child, indent + 1) + if (childResult.left.length === 0 && childResult.right.length === 0) continue + + const leftFirst = childResult.left[0]?.content ?? null + const rightFirst = childResult.right[0]?.content ?? null + const formatFirst = (content) => (content === null || content === '' ? '' : content.trimStart()) + + leftLines.push({ + type: childResult.left[0].type, + content: leftFirst === null || leftFirst === '' ? '' : `${childIndentStr}"${key}": ${formatFirst(leftFirst)}` + }) + rightLines.push({ + type: childResult.right[0].type, + content: rightFirst === null || rightFirst === '' ? '' : `${childIndentStr}"${key}": ${formatFirst(rightFirst)}` + }) + + const maxLength = Math.max(childResult.left.length, childResult.right.length) + for (let j = 1; j < maxLength; j++) { + leftLines.push( + j < childResult.left.length ? childResult.left[j] : { type: 'insert', content: '' } + ) + rightLines.push( + j < childResult.right.length ? childResult.right[j] : { type: 'delete', content: '' } + ) + } + + addTrailingComma(leftLines[leftLines.length - 1], commaLeft) + addTrailingComma(rightLines[rightLines.length - 1], commaRight) + } + + removeTrailingComma(commaLeft) + removeTrailingComma(commaRight) + leftLines.push({ type: displayType, content: indentStr + '}' }) + rightLines.push({ type: displayType, content: indentStr + '}' }) + return { left: leftLines, right: rightLines } + } + + // 处理 List(数组) + if (!isLeaf && isListType(nodeA) && isListType(nodeB)) { + const leftLines = [] + const rightLines = [] + const commaLeft = { current: undefined } + const commaRight = { current: undefined } + + leftLines.push({ type: displayType, content: indentStr + '[' }) + rightLines.push({ type: displayType, content: indentStr + '[' }) + + for (const match of comparison.matches) { + const childResult = convertComparisonToDisplay(match.comparison, indent + 1) + if (childResult.left.length === 0 && childResult.right.length === 0) continue + + const leftFirst = childResult.left[0]?.content + const rightFirst = childResult.right[0]?.content + const leftFirstLine = { + type: childResult.left[0].type, + content: leftFirst === undefined || leftFirst === '' ? '' : `${childIndentStr}${leftFirst.trimStart()}` + } + const rightFirstLine = { + type: childResult.right[0].type, + content: rightFirst === undefined || rightFirst === '' ? '' : `${childIndentStr}${rightFirst.trimStart()}` + } + leftLines.push(leftFirstLine) + rightLines.push(rightFirstLine) + + let leftLastLine = leftFirstLine + let rightLastLine = rightFirstLine + const maxLength = Math.max(childResult.left.length, childResult.right.length) + + for (let j = 1; j < maxLength; j++) { + const leftLine = j < childResult.left.length ? childResult.left[j] : { type: 'insert', content: '' } + const rightLine = j < childResult.right.length ? childResult.right[j] : { type: 'delete', content: '' } + if (leftLine.content !== '') leftLastLine = leftLine + if (rightLine.content !== '') rightLastLine = rightLine + leftLines.push(leftLine) + rightLines.push(rightLine) + } + + addTrailingComma(leftLastLine, commaLeft) + addTrailingComma(rightLastLine, commaRight) + } + + removeTrailingComma(commaLeft) + removeTrailingComma(commaRight) + leftLines.push({ type: displayType, content: indentStr + ']' }) + rightLines.push({ type: displayType, content: indentStr + ']' }) + return { left: leftLines, right: rightLines } + } + + // 处理叶子节点 + const leftLines = [] + const rightLines = [] + const leftData = nodeA !== undefined ? formatJsonData(nodeA, indent) : [''] + const rightData = nodeB !== undefined ? formatJsonData(nodeB, indent) : [''] + + if (displayType === 'different') { + for (let i = 0; i < leftData.length; i++) { + leftLines.push({ type: 'delete', content: leftData[i] }) + rightLines.push({ type: 'delete', content: '' }) + } + for (let i = 0; i < rightData.length; i++) { + leftLines.push({ type: 'insert', content: '' }) + rightLines.push({ type: 'insert', content: (i === 0 && rightData[i] !== '' ? indentStr : '') + rightData[i] }) + } + } else { + const maxLineSize = Math.max(leftData.length, rightData.length) + for (let i = 0; i < maxLineSize; i++) { + const leftContent = i < leftData.length ? leftData[i] : undefined + const rightContent = i < rightData.length ? (i === 0 && rightData[i] !== '' ? indentStr : '') + rightData[i] : undefined + leftLines.push({ type: displayType, content: leftContent ?? '' }) + rightLines.push({ type: displayType, content: rightContent ?? '' }) + } + } + return { left: leftLines, right: rightLines } +} + + +// 格式化JSON值为字符串 +export function formatJsonData(value, indent = 0) { + const indentStr = ' '.repeat(indent) + + if (value === null) { + return ["null"] + } + + if (typeof value === 'string') { + return [`"${value}"`] + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return [String(value)] + } + + if (Array.isArray(value)) { + if (value.length === 0) return ['[]'] + const lines = ["["] + for (let i = 0; i < value.length; i++) { + const itemLines = formatJsonData(value[i], indent + 1) + for (let j = 0; j < itemLines.length; j++) { + let content = j === 0 ? `${indentStr} ` : '' + content = content + itemLines[j] + if (i < value.length - 1 && j === itemLines.length - 1) { + content = content + ',' + } + lines.push(content) + } + } + lines.push(`${indentStr}]`) + return lines + } + + if (typeof value === 'object') { + const keys = Object.keys(value).sort() + if (keys.length === 0) return ['{}'] + const lines = ['{'] + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const val = value[key] + const itemLines = formatJsonData(val, indent + 1) + for (let j = 0; j < itemLines.length; j++) { + let content = j === 0 ? `${indentStr} "${key}": ` : '' + content = content + itemLines[j] + if (i < value.length - 1 && j === itemLines.length - 1) { + content = content + ',' + } + lines.push(content) + } + } + lines.push(`${indentStr}}`) + return lines + } + + return [String(value)] +} + +// 递归计算统计信息 +export function calculateStats(comparison) { + const stats = {same: 0, insert: 0, delete: 0, modify: 0} + const isLeaf = isLeafNodeOrNon(comparison.nodeA) || isLeafNodeOrNon(comparison.nodeB) + + if (comparison.type === NodeComparisonResult.DIFFERENT) { + // 不同节点:根据展示规则,左侧显示为删除,右侧显示为插入 + if (comparison.nodeA !== undefined && comparison.nodeB === undefined) { + stats.delete++ + } else if (comparison.nodeA === undefined && comparison.nodeB !== undefined) { + stats.insert++ + } else { + // 两个节点都存在但不同 + stats.delete++ // 左侧 + stats.insert++ // 右侧 + } + } else if (comparison.type === NodeComparisonResult.SAME || + comparison.type === NodeComparisonResult.SIMILAR) { + // 相同或相似节点 + if (isLeaf) { + // 叶子节点:视作修改 + if (comparison.type === NodeComparisonResult.SAME) { + stats.same++; + } else { + stats.modify++; + } + } else { + if (comparison.children) { + comparison.children.forEach(child => { + const childStats = calculateStats(child) + stats.same += childStats.same + stats.insert += childStats.insert + stats.delete += childStats.delete + stats.modify += childStats.modify + }) + } + + if (comparison.matches) { + comparison.matches.forEach(match => { + const matchStats = calculateStats(match.comparison) + stats.same += matchStats.same + stats.insert += matchStats.insert + stats.delete += matchStats.delete + stats.modify += matchStats.modify + }) + } + } + } + + return stats +} + +export function compareJson(textA, textB, ignoreListOrder = false) { + try { + const jsonA = JSON.parse(textA) + const jsonB = JSON.parse(textB) + const comparison = compareJsonNodes(jsonA, jsonB, ignoreListOrder) + const displayResult = convertComparisonToDisplay(comparison, 0) + const stats = calculateStats(comparison) + const maxLines = Math.max(displayResult.left.length, displayResult.right.length) + const leftLines = [] + const rightLines = [] + for (let i = 0; i < maxLines; i++) { + const leftLine = displayResult.left[i] || { type: 'same', content: '' } + const rightLine = displayResult.right[i] || { type: 'same', content: '' } + leftLines.push({ type: leftLine.type, content: leftLine.content, lineNumber: i + 1 }) + rightLines.push({ type: rightLine.type, content: rightLine.content, lineNumber: i + 1 }) + } + return { left: leftLines, right: rightLines, stats } + } catch (e) { + throw new Error('JSON解析失败:' + e.message) + } +} diff --git a/src/utils/comparator/textDiff.js b/src/utils/comparator/textDiff.js new file mode 100644 index 0000000..1c07caa --- /dev/null +++ b/src/utils/comparator/textDiff.js @@ -0,0 +1,375 @@ +import { escapeHtml } from './html.js' + +export function shouldMergeAsModify(lineA, lineB) { + // 如果两行完全相同,不应该到达这里 + if (lineA === lineB) return false + + // 对于JSON格式的行,检查是否是同一类型的结构 + // 例如:都是键值对,但值不同;或者都是数组元素等 + const trimmedA = lineA.trim() + const trimmedB = lineB.trim() + + // 如果都是键值对格式("key": value),且键相同,认为是修改 + const keyValuePattern = /^\s*"([^"]+)":\s*(.+)$/ + const matchA = trimmedA.match(keyValuePattern) + const matchB = trimmedB.match(keyValuePattern) + + if (matchA && matchB) { + // 如果键相同,认为是修改 + if (matchA[1] === matchB[1]) { + return true + } + // 如果键不同,认为是删除+插入 + return false + } + + // 如果都是数组元素或对象结构,检查结构相似性 + // 这里简化处理:如果行结构相似(都有相同的括号、引号等),可能是修改 + const structureA = trimmedA.replace(/"[^"]*"/g, '"..."').replace(/\d+/g, '0') + const structureB = trimmedB.replace(/"[^"]*"/g, '"..."').replace(/\d+/g, '0') + + // 如果结构相似度较高,认为是修改 + // 这里使用简单的启发式:如果结构字符串的前几个字符相同 + if (structureA.length > 0 && structureB.length > 0) { + const minLen = Math.min(structureA.length, structureB.length) + let samePrefix = 0 + for (let k = 0; k < minLen && k < 10; k++) { + if (structureA[k] === structureB[k]) { + samePrefix++ + } else { + break + } + } + // 如果前缀相似度超过50%,认为是修改 + if (samePrefix / minLen > 0.5) { + return true + } + } + + // 默认不合并为修改,分别显示为删除和插入 + return false +} + +export function computeDiff(arrA, arrB) { + const n = arrA.length + const m = arrB.length + + // 使用动态规划计算LCS + const dp = Array(n + 1).fill(null).map(() => Array(m + 1).fill(0)) + + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (arrA[i - 1] === arrB[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + } + } + } + + // 回溯找出所有匹配点。当同一字符在 B 中出现多次时,优先选 j 更小的(靠左匹配) + const matches = [] + let i = n, j = m + + while (i > 0 && j > 0) { + if (arrA[i - 1] === arrB[j - 1]) { + // 若 dp[i][j-1] === dp[i][j],说明不匹配 (i-1,j-1) 也能得到同样长的 LCS,可先 j-- 尝试更靠左的匹配 + if (j > 1 && dp[i][j - 1] === dp[i][j]) { + j-- + } else { + matches.unshift({x: i - 1, y: j - 1}) + i-- + j-- + } + } else if (dp[i - 1][j] > dp[i][j - 1]) { + i-- + } else { + j-- + } + } + + // 构建路径:每个匹配点都要加入,否则 compareTextByChar 会漏掉匹配(如 hello vs helloworld 的 o) + const path = [] + for (const match of matches) { + path.push({x: match.x, y: match.y}) + } + if (path.length > 0) { + const last = path[path.length - 1] + if (last.x < n || last.y < m) { + path.push({x: n, y: m}) + } + } else { + path.push({x: n, y: m}) + } + + return path +} + +export function compareTextByLine(textA, textB) { + const linesA = textA.split('\n') + const linesB = textB.split('\n') + + const n = linesA.length + const m = linesB.length + + // 使用动态规划计算LCS + const dp = Array(n + 1).fill(null).map(() => Array(m + 1).fill(0)) + const path = Array(n + 1).fill(null).map(() => Array(m + 1).fill(null)) + + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (linesA[i - 1] === linesB[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + path[i][j] = 'match' + } else if (dp[i - 1][j] > dp[i][j - 1]) { + dp[i][j] = dp[i - 1][j] + path[i][j] = 'delete' + } else { + dp[i][j] = dp[i][j - 1] + path[i][j] = 'insert' + } + } + } + + // 回溯构建结果 + const resultA = [] + const resultB = [] + let stats = {same: 0, insert: 0, delete: 0, modify: 0} + + let i = n, j = m + let lineNumA = n, lineNumB = m + const pendingDelete = [] + const pendingInsert = [] + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && path[i][j] === 'match') { + // 处理待处理的删除和插入 + // 只有在内容相似时才合并为修改 + while (pendingDelete.length > 0 && pendingInsert.length > 0) { + const deleteLine = pendingDelete[0] + const insertLine = pendingInsert[0] + if (shouldMergeAsModify(deleteLine, insertLine)) { + resultA.unshift({type: 'modify', content: pendingDelete.shift(), lineNumber: lineNumA--}) + resultB.unshift({type: 'modify', content: pendingInsert.shift(), lineNumber: lineNumB--}) + stats.modify++ + } else { + // 不相似,分别显示为删除和插入 + resultA.unshift({type: 'delete', content: pendingDelete.shift(), lineNumber: lineNumA--}) + resultB.unshift({type: 'delete', content: '', lineNumber: null}) + stats.delete++ + resultA.unshift({type: 'insert', content: '', lineNumber: null}) + resultB.unshift({type: 'insert', content: pendingInsert.shift(), lineNumber: lineNumB--}) + stats.insert++ + } + } + // 处理剩余的删除 + while (pendingDelete.length > 0) { + resultA.unshift({type: 'delete', content: pendingDelete.shift(), lineNumber: lineNumA--}) + resultB.unshift({type: 'delete', content: '', lineNumber: null}) + stats.delete++ + } + // 处理剩余的插入 + while (pendingInsert.length > 0) { + resultA.unshift({type: 'insert', content: '', lineNumber: null}) + resultB.unshift({type: 'insert', content: pendingInsert.shift(), lineNumber: lineNumB--}) + stats.insert++ + } + // 添加匹配的行 + resultA.unshift({type: 'same', content: linesA[i - 1], lineNumber: lineNumA}) + resultB.unshift({type: 'same', content: linesB[j - 1], lineNumber: lineNumB}) + stats.same++ + i-- + j-- + lineNumA-- + lineNumB-- + } else if (i > 0 && path[i][j] === 'delete') { + pendingDelete.unshift(linesA[i - 1]) + i-- + lineNumA-- + } else if (j > 0 && path[i][j] === 'insert') { + pendingInsert.unshift(linesB[j - 1]) + j-- + lineNumB-- + } else if (i > 0) { + pendingDelete.unshift(linesA[i - 1]) + i-- + lineNumA-- + } else if (j > 0) { + pendingInsert.unshift(linesB[j - 1]) + j-- + lineNumB-- + } else { + break + } + } + + // 处理剩余的待处理项 + while (pendingDelete.length > 0 && pendingInsert.length > 0) { + const deleteLine = pendingDelete[0] + const insertLine = pendingInsert[0] + if (shouldMergeAsModify(deleteLine, insertLine)) { + resultA.unshift({type: 'modify', content: pendingDelete.shift(), lineNumber: lineNumA--}) + resultB.unshift({type: 'modify', content: pendingInsert.shift(), lineNumber: lineNumB--}) + stats.modify++ + } else { + // 不相似,分别显示为删除和插入 + resultA.unshift({type: 'delete', content: pendingDelete.shift(), lineNumber: lineNumA--}) + resultB.unshift({type: 'delete', content: '', lineNumber: null}) + stats.delete++ + resultA.unshift({type: 'insert', content: '', lineNumber: null}) + resultB.unshift({type: 'insert', content: pendingInsert.shift(), lineNumber: lineNumB--}) + stats.insert++ + } + } + while (pendingDelete.length > 0) { + resultA.unshift({type: 'delete', content: pendingDelete.shift(), lineNumber: lineNumA--}) + resultB.unshift({type: 'delete', content: '', lineNumber: null}) + stats.delete++ + } + while (pendingInsert.length > 0) { + resultA.unshift({type: 'insert', content: '', lineNumber: null}) + resultB.unshift({type: 'insert', content: pendingInsert.shift(), lineNumber: lineNumB--}) + stats.insert++ + } + + return {left: resultA, right: resultB, stats} +} + +export function compareTextByChar(textA, textB) { + const linesA = textA.split('\n') + const linesB = textB.split('\n') + + const resultA = [] + const resultB = [] + let stats = {same: 0, insert: 0, delete: 0, modify: 0} + + const maxLines = Math.max(linesA.length, linesB.length) + + for (let i = 0; i < maxLines; i++) { + const lineA = linesA[i] || '' + const lineB = linesB[i] || '' + + if (lineA === lineB) { + resultA.push({type: 'same', content: lineA, html: escapeHtml(lineA), lineNumber: i + 1}) + resultB.push({type: 'same', content: lineB, html: escapeHtml(lineB), lineNumber: i + 1}) + stats.same += lineA.length + } else { + // 按字符进行diff,统计按高亮单元(字符)计数 + const charsA = lineA.split('') + const charsB = lineB.split('') + + const charDiff = computeDiff(charsA, charsB) + + let htmlA = '' + let htmlB = '' + let x = 0, y = 0 + let hasDiff = false + + for (let j = 0; j < charDiff.length; j++) { + const point = charDiff[j] + const nextPoint = charDiff[j + 1] || {x: charsA.length, y: charsB.length} + + // 从 (x,y) 到匹配点 (point.x, point.y) 之前:只可能是差异(如 na vs aa 中 (0,0) 是修改不是相同) + while (x < point.x || y < point.y) { + if (x < point.x && y < point.y) { + htmlA += `${escapeHtml(charsA[x])}` + htmlB += `${escapeHtml(charsB[y])}` + stats.modify++ + hasDiff = true + x++ + y++ + } else if (x < point.x) { + htmlA += `${escapeHtml(charsA[x])}` + htmlB += '' + stats.delete++ + hasDiff = true + x++ + } else { + htmlA += '' + htmlB += `${escapeHtml(charsB[y])}` + stats.insert++ + hasDiff = true + y++ + } + } + // 仅匹配点 (point.x, point.y) 为相同(终点 (n,m) 不是匹配点,不输出) + if (x === point.x && y === point.y && point.x < charsA.length && point.y < charsB.length) { + htmlA += escapeHtml(charsA[x]) + htmlB += escapeHtml(charsB[y]) + stats.same++ + x++ + y++ + } + + // 差异的字符:修改用黄色,仅左为删除(红),仅右为插入(蓝) + if (x < nextPoint.x && y < nextPoint.y) { + htmlA += `${escapeHtml(charsA[x])}` + htmlB += `${escapeHtml(charsB[y])}` + stats.modify++ + hasDiff = true + x++ + y++ + } else if (x < nextPoint.x) { + htmlA += `${escapeHtml(charsA[x])}` + htmlB += '' + stats.delete++ + hasDiff = true + x++ + } else if (y < nextPoint.y) { + htmlA += '' + htmlB += `${escapeHtml(charsB[y])}` + stats.insert++ + hasDiff = true + y++ + } + } + + // 处理剩余字符 + while (x < charsA.length && y < charsB.length) { + if (charsA[x] === charsB[y]) { + htmlA += escapeHtml(charsA[x]) + htmlB += escapeHtml(charsB[y]) + stats.same++ + } else { + htmlA += `${escapeHtml(charsA[x])}` + htmlB += `${escapeHtml(charsB[y])}` + stats.modify++ + hasDiff = true + } + x++ + y++ + } + + while (x < charsA.length) { + htmlA += `${escapeHtml(charsA[x])}` + stats.delete++ + hasDiff = true + x++ + } + + while (y < charsB.length) { + htmlB += `${escapeHtml(charsB[y])}` + stats.insert++ + hasDiff = true + y++ + } + + resultA.push({ + type: hasDiff ? 'modify' : (lineA ? 'delete' : 'insert'), + content: lineA, + html: htmlA || escapeHtml(lineA), + lineNumber: i + 1, + inlineHighlight: hasDiff + }) + resultB.push({ + type: hasDiff ? 'modify' : (lineB ? 'insert' : 'delete'), + content: lineB, + html: htmlB || escapeHtml(lineB), + lineNumber: i + 1, + inlineHighlight: hasDiff + }) + } + } + + return {left: resultA, right: resultB, stats} +} diff --git a/src/utils/encoder.js b/src/utils/encoder.js new file mode 100644 index 0000000..6eda2c0 --- /dev/null +++ b/src/utils/encoder.js @@ -0,0 +1,117 @@ + +export function bytesToBase64(bytes) { + let binary = '' + for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]) + return btoa(binary) +} + +export function base64ToBytes(str) { + const clean = str.replace(/\s/g, '') + const binary = atob(clean) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + return bytes +} + +export function encodeBase64(text) { + return btoa(unescape(encodeURIComponent(text))) +} + +export function decodeBase64(text) { + return decodeURIComponent(escape(atob(text))) +} + +export function encodeUrl(text) { + return encodeURIComponent(text) +} + +export function decodeUrl(text) { + return decodeURIComponent(text) +} + +export function encodeUnicode(text) { + let result = '' + let i = 0 + + while (i < text.length) { + const codePoint = text.codePointAt(i) + + if (codePoint > 0xFFFF) { + // 超出 BMP 的字符,使用代理对表示 + const high = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800 + const low = ((codePoint - 0x10000) % 0x400) + 0xDC00 + result += '\\u' + high.toString(16).toUpperCase().padStart(4, '0') + result += '\\u' + low.toString(16).toUpperCase().padStart(4, '0') + i += 2 // 代理对占用两个字符位置 + } else { + // BMP 字符 + result += '\\u' + codePoint.toString(16).toUpperCase().padStart(4, '0') + i++ + } + } + + return result +} + +export function decodeUnicode(text) { + try { + let result = '' + let i = 0 + + while (i < text.length) { + // 匹配 \uXXXX 格式 + if (text[i] === '\\' && i + 1 < text.length && text[i + 1] === 'u' && i + 5 < text.length) { + const hex = text.substring(i + 2, i + 6) + if (/^[0-9a-fA-F]{4}$/.test(hex)) { + const code1 = parseInt(hex, 16) + + // 检查是否是高代理(surrogate high) + if (code1 >= 0xD800 && code1 <= 0xDBFF && i + 11 < text.length) { + // 检查下一个是否是低代理 + if (text[i + 6] === '\\' && text[i + 7] === 'u') { + const hex2 = text.substring(i + 8, i + 12) + if (/^[0-9a-fA-F]{4}$/.test(hex2)) { + const code2 = parseInt(hex2, 16) + // 检查是否是低代理(surrogate low) + if (code2 >= 0xDC00 && code2 <= 0xDFFF) { + // 组合代理对 + const codePoint = 0x10000 + ((code1 - 0xD800) << 10) + (code2 - 0xDC00) + result += String.fromCodePoint(codePoint) + i += 12 + continue + } + } + } + } + + // 普通字符或单独的代理 + result += String.fromCharCode(code1) + i += 6 + continue + } + } + + // 匹配 \UXXXXXXXX 格式(8位十六进制) + if (text[i] === '\\' && i + 1 < text.length && text[i + 1] === 'U' && i + 9 < text.length) { + const hex = text.substring(i + 2, i + 10) + if (/^[0-9a-fA-F]{8}$/.test(hex)) { + const code = parseInt(hex, 16) + if (code > 0x10FFFF) { + throw new Error('无效的 Unicode 码点:超出范围') + } + result += String.fromCodePoint(code) + i += 10 + continue + } + } + + // 普通字符 + result += text[i] + i++ + } + + return result + } catch (e) { + throw new Error('Unicode 解码失败:' + e.message) + } +} diff --git a/src/utils/jsonFormatter/jsonPath.js b/src/utils/jsonFormatter/jsonPath.js new file mode 100644 index 0000000..06c14a7 --- /dev/null +++ b/src/utils/jsonFormatter/jsonPath.js @@ -0,0 +1,142 @@ +export function parseJsonPath(jsonPath) { + if (!jsonPath || !jsonPath.trim()) return null + + const path = jsonPath.trim() + // 移除开头的 $ 或 $. + const normalizedPath = path.replace(/^\$\.?/, '') + if (!normalizedPath) return [] + + // 解析路径段:支持 .key 和 [index] 或 [*] + const segments = [] + let current = normalizedPath + let i = 0 + + while (i < current.length) { + if (current[i] === '[') { + // 数组索引 + const endIndex = current.indexOf(']', i) + if (endIndex === -1) break + const indexStr = current.substring(i + 1, endIndex) + if (indexStr === '*') { + segments.push({ type: 'wildcard', index: '*' }) + } else { + const index = parseInt(indexStr, 10) + if (!isNaN(index)) { + segments.push({ type: 'index', index }) + } + } + i = endIndex + 1 + } else if (current[i] === '.') { + i++ + } else { + // 对象键 + let keyEnd = i + while (keyEnd < current.length && current[keyEnd] !== '.' && current[keyEnd] !== '[') { + keyEnd++ + } + const key = current.substring(i, keyEnd) + if (key) { + segments.push({ type: 'key', key }) + } + i = keyEnd + } + } + + return segments +} + +export function pathToJsonPath(path) { + if (path === 'root') return '$' + return '$' + path.replace(/^root/, '') +} + +export function pathMatchesJsonPath(path, jsonPathSegments) { + if (!jsonPathSegments || jsonPathSegments.length === 0) return true + + // 将路径转换为段数组 + const pathSegments = [] + const pathStr = path === 'root' ? '' : path.replace(/^root\.?/, '') + + if (!pathStr) { + return jsonPathSegments.length === 0 + } + + // 解析路径段 + let current = pathStr + let i = 0 + + while (i < current.length) { + if (current[i] === '[') { + const endIndex = current.indexOf(']', i) + if (endIndex === -1) break + const indexStr = current.substring(i + 1, endIndex) + const index = parseInt(indexStr, 10) + if (!isNaN(index)) { + pathSegments.push({ type: 'index', index }) + } + i = endIndex + 1 + } else if (current[i] === '.') { + i++ + } else { + let keyEnd = i + while (keyEnd < current.length && current[keyEnd] !== '.' && current[keyEnd] !== '[') { + keyEnd++ + } + const key = current.substring(i, keyEnd) + if (key) { + pathSegments.push({ type: 'key', key }) + } + i = keyEnd + } + } + + // 精确匹配:路径段数必须等于 JSONPath 段数 + if (pathSegments.length !== jsonPathSegments.length) return false + + for (let i = 0; i < jsonPathSegments.length; i++) { + const jsonSeg = jsonPathSegments[i] + const pathSeg = pathSegments[i] + + if (!pathSeg) return false + + if (jsonSeg.type === 'wildcard') { + // 通配符匹配任何索引 + if (pathSeg.type !== 'index') return false + } else if (jsonSeg.type === 'index') { + if (pathSeg.type !== 'index' || pathSeg.index !== jsonSeg.index) return false + } else if (jsonSeg.type === 'key') { + if (pathSeg.type !== 'key' || pathSeg.key !== jsonSeg.key) return false + } + } + + return true +} + +export function getDataByPath(obj, path) { + if (path === 'root') return obj + + const pathStr = path.replace(/^root\.?/, '') + let current = obj + let i = 0 + + while (i < pathStr.length && current !== undefined && current !== null) { + if (pathStr[i] === '[') { + const endIdx = pathStr.indexOf(']', i) + const idx = parseInt(pathStr.substring(i + 1, endIdx), 10) + current = current[idx] + i = endIdx + 1 + } else if (pathStr[i] === '.') { + i++ + } else { + let keyEnd = i + while (keyEnd < pathStr.length && pathStr[keyEnd] !== '.' && pathStr[keyEnd] !== '[') { + keyEnd++ + } + const key = pathStr.substring(i, keyEnd) + current = current[key] + i = keyEnd + } + } + + return current +} diff --git a/src/utils/timestamp.js b/src/utils/timestamp.js new file mode 100644 index 0000000..2092df9 --- /dev/null +++ b/src/utils/timestamp.js @@ -0,0 +1,74 @@ + +export function parseTimestampInput(value, type) { + const timestampStr = value.trim() + if (!timestampStr) return { error: 'empty' } + + if (type === 'nanoseconds') { + try { + const timestampNs = BigInt(timestampStr) + const timestampMs = Number(timestampNs / BigInt(1000000)) + const nanoseconds = Number(timestampNs % BigInt(1000000)) + return { timestampMs, nanoseconds } + } catch { + return { error: 'invalid_nanoseconds' } + } + } + + const timestamp = parseInt(timestampStr, 10) + if (isNaN(timestamp)) return { error: 'invalid_number' } + + if (type === 'seconds') { + const timestampMs = timestamp.toString().length <= 10 ? timestamp * 1000 : timestamp + return { timestampMs, nanoseconds: 0 } + } + + return { timestampMs: timestamp, nanoseconds: 0 } +} + +export function formatTimestampMs(timestampMs, type, nanoseconds = 0) { + const date = new Date(timestampMs) + if (isNaN(date.getTime())) return { error: 'invalid_date' } + + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + const milliseconds = String(date.getMilliseconds()).padStart(3, '0') + + if (type === 'seconds') { + return { value: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } + } + if (type === 'milliseconds') { + return { value: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}` } + } + const nanosecondsStr = String(nanoseconds).padStart(6, '0') + return { value: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}${nanosecondsStr}` } +} + +export function parseDateString(dateStr) { + const trimmed = dateStr.trim() + if (!trimmed) return { error: 'empty' } + + let date = null + if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(trimmed)) { + date = new Date(trimmed.replace(' ', 'T')) + } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+$/.test(trimmed)) { + date = new Date(trimmed.replace(' ', 'T')) + } else if (/^\d{4}\/\d{2}\/\d{2}/.test(trimmed)) { + date = new Date(trimmed) + } else { + date = new Date(trimmed) + } + + if (!date || isNaN(date.getTime())) return { error: 'invalid_date' } + return { date } +} + +export function dateToTimestamp(date, type) { + const ms = date.getTime() + if (type === 'seconds') return { value: String(Math.floor(ms / 1000)) } + if (type === 'milliseconds') return { value: String(ms) } + return { value: String(BigInt(ms) * BigInt(1000000)) } +} diff --git a/src/utils/variableName.js b/src/utils/variableName.js new file mode 100644 index 0000000..f2550d1 --- /dev/null +++ b/src/utils/variableName.js @@ -0,0 +1,85 @@ +export function parseToWords(text) { + if (!text || !text.trim()) { + return [] + } + + let processed = text.trim() + + // 处理各种分隔符:空格、下划线、横线、驼峰 + // 1. 先处理连续大写字母的情况:XMLHttpRequest -> XML Http Request + processed = processed.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') + + // 2. 先处理数字和字母的边界(必须在驼峰处理之前) + // 2.1 字母+数字+字母:temp2Detail -> temp 2 Detail + processed = processed.replace(/([a-zA-Z])(\d+)([a-zA-Z])/g, '$1 $2 $3') + // 2.2 字母+数字(后面跟着分隔符或结尾,但不是字母):item2 -> item 2 + // 注意:这里不匹配后面跟着字母的情况(已由2.1处理) + processed = processed.replace(/([a-zA-Z])(\d+)(?=[_\-\s]|$)/g, '$1 $2') + // 2.3 数字+字母(在单词开头或前面是分隔符):2item -> 2 item + processed = processed.replace(/(\d+)([a-zA-Z])/g, '$1 $2') + + // 3. 处理驼峰:camelCase -> camel Case(在数字处理之后) + processed = processed.replace(/([a-z])([A-Z])/g, '$1 $2') + + // 4. 统一分隔符:下划线、横线、空格统一为空格 + processed = processed.replace(/[_\-\s]+/g, ' ') + + // 5. 分割并处理 + let words = processed + .split(' ') + .filter(word => word.length > 0) + .map(word => { + // 转换为小写,保留字母和数字 + return word.toLowerCase() + }) + .filter(word => word.length > 0) // 允许纯数字 + + return words +} + +// 转换单词首字母为大写(处理数字情况) +export function capitalizeWord(word) { + if (!word) return '' + // 如果单词是纯数字,直接返回 + if (/^\d+$/.test(word)) return word + // 否则首字母大写 + return word.charAt(0).toUpperCase() + word.slice(1) +} + +// 转换为小驼峰 (camelCase) +export function toCamelCase(words) { + if (words.length === 0) return '' + + const firstWord = words[0] + const restWords = words.slice(1).map(word => capitalizeWord(word)) + + return firstWord + restWords.join('') +} + +// 转换为大驼峰 (PascalCase) +export function toPascalCase(words) { + if (words.length === 0) return '' + + return words.map(word => capitalizeWord(word)).join('') +} + +// 转换为下划线 (snake_case) +export function toSnakeCase(words) { + if (words.length === 0) return '' + + return words.join('_') +} + +// 转换为横线 (kebab-case) +export function toKebabCase(words) { + if (words.length === 0) return '' + + return words.join('-') +} + +// 转换为常量 (CONSTANT_CASE) +export function toConstantCase(words) { + if (words.length === 0) return '' + + return words.map(word => word.toUpperCase()).join('_') +} \ No newline at end of file diff --git a/src/views/ColorConverter.vue b/src/views/ColorConverter.vue new file mode 100644 index 0000000..f221da4 --- /dev/null +++ b/src/views/ColorConverter.vue @@ -0,0 +1,1482 @@ + + + + + diff --git a/src/views/Comparator.vue b/src/views/Comparator.vue new file mode 100644 index 0000000..722211d --- /dev/null +++ b/src/views/Comparator.vue @@ -0,0 +1,1472 @@ + + + + + + + diff --git a/src/views/EncoderDecoder.vue b/src/views/EncoderDecoder.vue new file mode 100644 index 0000000..0d8827e --- /dev/null +++ b/src/views/EncoderDecoder.vue @@ -0,0 +1,1172 @@ + + + + + diff --git a/src/views/Home.vue b/src/views/Home.vue new file mode 100644 index 0000000..7b79c27 --- /dev/null +++ b/src/views/Home.vue @@ -0,0 +1,201 @@ + + + + + + diff --git a/src/views/JsonFormatter.vue b/src/views/JsonFormatter.vue new file mode 100644 index 0000000..6121600 --- /dev/null +++ b/src/views/JsonFormatter.vue @@ -0,0 +1,1953 @@ + + + + + diff --git a/src/views/QRCodeGenerator.vue b/src/views/QRCodeGenerator.vue new file mode 100644 index 0000000..a2cdf55 --- /dev/null +++ b/src/views/QRCodeGenerator.vue @@ -0,0 +1,700 @@ + + + + + \ No newline at end of file diff --git a/src/views/TimestampConverter.vue b/src/views/TimestampConverter.vue new file mode 100644 index 0000000..46dd82b --- /dev/null +++ b/src/views/TimestampConverter.vue @@ -0,0 +1,913 @@ + + + + + diff --git a/src/views/VariableNameConverter.vue b/src/views/VariableNameConverter.vue new file mode 100644 index 0000000..892f7d9 --- /dev/null +++ b/src/views/VariableNameConverter.vue @@ -0,0 +1,526 @@ + + + + + + diff --git a/tests/unit/byteUtils.test.js b/tests/unit/byteUtils.test.js new file mode 100644 index 0000000..c05c460 --- /dev/null +++ b/tests/unit/byteUtils.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { getByteLength, truncateToMaxBytes } from '../../src/utils/byteUtils.js' + +describe('byteUtils', () => { + it('counts ASCII bytes', () => { + expect(getByteLength('hello')).toBe(5) + expect(getByteLength('')).toBe(0) + }) + + it('counts UTF-8 multibyte characters', () => { + expect(getByteLength('中')).toBe(3) + expect(getByteLength('hello世界')).toBe(11) + }) + + it('returns original string when within limit', () => { + expect(truncateToMaxBytes('hello', 10)).toBe('hello') + }) + + it('truncates by bytes without breaking multibyte chars', () => { + const text = 'a'.repeat(10) + '中' + const truncated = truncateToMaxBytes(text, 11) + expect(getByteLength(truncated)).toBeLessThanOrEqual(11) + expect(truncated).not.toContain('中') + }) + + it('handles exact byte boundary', () => { + expect(truncateToMaxBytes('ab', 2)).toBe('ab') + expect(truncateToMaxBytes('abc', 2)).toBe('ab') + }) +}) diff --git a/tests/unit/color.test.js b/tests/unit/color.test.js new file mode 100644 index 0000000..fbf74e3 --- /dev/null +++ b/tests/unit/color.test.js @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest' +import { rgbToHex, rgbToHsl, hexToRgb, hslToRgb } from '../../src/utils/color.js' + +describe('color conversion', () => { + it('converts RGB to hex', () => { + expect(rgbToHex(255, 0, 0)).toBe('FF0000') + expect(rgbToHex(0, 255, 0)).toBe('00FF00') + expect(rgbToHex(0, 0, 255)).toBe('0000FF') + }) + + it('pads single-digit hex values', () => { + expect(rgbToHex(1, 2, 3)).toBe('010203') + }) + + it('clamps RGB values to 0-255', () => { + expect(rgbToHex(300, -10, 128)).toBe('FF0080') + }) + + it('converts hex to RGB', () => { + expect(hexToRgb('FF0000')).toEqual({ r: 255, g: 0, b: 0 }) + expect(hexToRgb('invalid')).toBeNull() + }) + + it('converts RGB to HSL and back (gray)', () => { + const hsl = rgbToHsl(128, 128, 128) + expect(hsl.s).toBe(0) + const rgb = hslToRgb(hsl.h, hsl.s, hsl.l) + expect(rgb.r).toBeCloseTo(128, 0) + expect(rgb.g).toBeCloseTo(128, 0) + expect(rgb.b).toBeCloseTo(128, 0) + }) + + it('converts pure red RGB <-> HSL', () => { + const hsl = rgbToHsl(255, 0, 0) + expect(hsl.h).toBe(0) + expect(hsl.s).toBe(100) + const rgb = hslToRgb(0, 100, 50) + expect(rgb.r).toBe(255) + expect(rgb.g).toBe(0) + expect(rgb.b).toBe(0) + }) +}) diff --git a/tests/unit/comparator/html.test.js b/tests/unit/comparator/html.test.js new file mode 100644 index 0000000..7479e26 --- /dev/null +++ b/tests/unit/comparator/html.test.js @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { escapeHtml, highlightDiff } from '../../../src/utils/comparator/html.js' + +describe('comparator/html', () => { + describe('escapeHtml', () => { + it('escapes special HTML characters', () => { + expect(escapeHtml('')).toBe( + '<script>"'&</script>' + ) + }) + + it('handles empty string', () => { + expect(escapeHtml('')).toBe('') + }) + + it('coerces non-string values', () => { + expect(escapeHtml(123)).toBe('123') + }) + }) + + describe('highlightDiff', () => { + it('returns escaped text when no ranges', () => { + expect(highlightDiff('hello', [])).toBe('hello') + expect(highlightDiff('a { + const result = highlightDiff('hello', [{ start: 1, end: 4 }]) + expect(result).toBe('hello') + }) + + it('handles multiple ranges', () => { + const result = highlightDiff('abcdef', [ + { start: 0, end: 1 }, + { start: 4, end: 6 }, + ]) + expect(result).toContain('abcd') + expect(result).toContain('ef') + }) + }) +}) diff --git a/tests/unit/comparator/jsonCompare.test.js b/tests/unit/comparator/jsonCompare.test.js new file mode 100644 index 0000000..132644c --- /dev/null +++ b/tests/unit/comparator/jsonCompare.test.js @@ -0,0 +1,243 @@ +import { describe, it, expect } from 'vitest' +import { + NodeComparisonResult, + compareJsonNodes, + compareJson, + stringSimilarity, + calculateStats, + formatJsonData, +} from '../../../src/utils/comparator/jsonCompare.js' + +describe('comparator/jsonCompare', () => { + describe('stringSimilarity', () => { + it('returns same for identical strings', () => { + expect(stringSimilarity('hello', 'hello')).toEqual({ type: 'same', similarity: 1.0 }) + }) + + it('returns same for two empty strings', () => { + expect(stringSimilarity('', '')).toEqual({ type: 'same', similarity: 1.0 }) + }) + + it('returns different for completely unrelated strings', () => { + expect(stringSimilarity('abc', 'xyz')).toEqual({ type: 'different', similarity: 0.0 }) + }) + + it('returns similar for partially matching strings', () => { + const result = stringSimilarity('hello', 'hallo') + expect(result.type).toBe('similar') + expect(result.similarity).toBeGreaterThan(0) + expect(result.similarity).toBeLessThan(1) + }) + }) + + describe('compareJsonNodes', () => { + it('compares identical primitives', () => { + expect(compareJsonNodes(1, 1).type).toBe(NodeComparisonResult.SAME) + expect(compareJsonNodes(true, true).type).toBe(NodeComparisonResult.SAME) + expect(compareJsonNodes(false, false).type).toBe(NodeComparisonResult.SAME) + }) + + it('compares different numbers as DIFFERENT', () => { + expect(compareJsonNodes(1, 2).type).toBe(NodeComparisonResult.DIFFERENT) + }) + + it('treats null === null as SAME', () => { + expect(compareJsonNodes(null, null).type).toBe(NodeComparisonResult.SAME) + }) + + it('treats null vs undefined as DIFFERENT', () => { + expect(compareJsonNodes(null, undefined).type).toBe(NodeComparisonResult.DIFFERENT) + }) + + it('treats null vs value as DIFFERENT', () => { + expect(compareJsonNodes(null, 1).type).toBe(NodeComparisonResult.DIFFERENT) + expect(compareJsonNodes(1, null).type).toBe(NodeComparisonResult.DIFFERENT) + }) + + it('compares strings with similarity', () => { + const result = compareJsonNodes('hello', 'hallo') + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + }) + + it('compares different types as DIFFERENT', () => { + expect(compareJsonNodes(1, '1').type).toBe(NodeComparisonResult.DIFFERENT) + expect(compareJsonNodes([], {}).type).toBe(NodeComparisonResult.DIFFERENT) + }) + + it('compares empty objects as SAME', () => { + expect(compareJsonNodes({}, {}).type).toBe(NodeComparisonResult.SAME) + }) + + it('compares empty arrays as SAME', () => { + expect(compareJsonNodes([], []).type).toBe(NodeComparisonResult.SAME) + }) + + it('detects added key as SIMILAR map', () => { + const result = compareJsonNodes({ a: 1 }, { a: 1, b: 2 }) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + expect(result.children.some(c => c.key === 'b')).toBe(true) + }) + + it('detects removed key', () => { + const result = compareJsonNodes({ a: 1, b: 2 }, { a: 1 }) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + }) + + it('detects changed value for same key as SIMILAR', () => { + const result = compareJsonNodes({ name: 'Alice' }, { name: 'Bob' }) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + const nameComp = result.children.find(c => c.key === 'name') + expect(nameComp.type).toBe(NodeComparisonResult.SIMILAR) + }) + + it('compares nested objects', () => { + const a = { user: { name: 'Alice', age: 30 } } + const b = { user: { name: 'Alice', age: 31 } } + const result = compareJsonNodes(a, b) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + }) + }) + + describe('compareJsonNodes list order', () => { + it('preserves order by default (compareLists)', () => { + const result = compareJsonNodes([1, 2, 3], [1, 3, 2], false) + expect(result.type).not.toBe(NodeComparisonResult.SAME) + }) + + it('ignores order when ignoreOrder=true', () => { + const result = compareJsonNodes([1, 2, 3], [3, 2, 1], true) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + expect(result.similarity).toBeGreaterThan(0) + expect(result.matches.filter(m => m.indexA !== undefined && m.indexB !== undefined)).toHaveLength(3) + }) + + it('matches reorderable arrays with same elements', () => { + const result = compareJsonNodes( + [{ id: 1 }, { id: 2 }], + [{ id: 2 }, { id: 1 }], + true + ) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + expect(result.matches.filter(m => m.indexA !== undefined && m.indexB !== undefined)).toHaveLength(2) + }) + + it('detects element addition in ordered list', () => { + const result = compareJsonNodes([1, 2], [1, 2, 3], false) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + }) + + it('handles empty vs non-empty array', () => { + const result = compareJsonNodes([], [1]) + expect(result.type).toBe(NodeComparisonResult.SIMILAR) + }) + }) + + describe('calculateStats', () => { + it('counts same leaf nodes', () => { + const comp = compareJsonNodes({ a: 1 }, { a: 1 }) + const stats = calculateStats(comp) + expect(stats.same).toBeGreaterThan(0) + }) + + it('counts modify for changed string values', () => { + const comp = compareJsonNodes({ name: 'Alice' }, { name: 'Bob' }) + const stats = calculateStats(comp) + expect(stats.modify).toBeGreaterThan(0) + }) + + it('counts insert/delete for added/removed keys', () => { + const comp = compareJsonNodes({ a: 1 }, { a: 1, b: 2 }) + const stats = calculateStats(comp) + expect(stats.insert).toBeGreaterThan(0) + }) + }) + + describe('formatJsonData', () => { + it('formats null', () => { + expect(formatJsonData(null)).toEqual(['null']) + }) + + it('formats string with quotes', () => { + expect(formatJsonData('hello')).toEqual(['"hello"']) + }) + + it('formats number and boolean', () => { + expect(formatJsonData(42)).toEqual(['42']) + expect(formatJsonData(true)).toEqual(['true']) + }) + + it('formats empty array and object', () => { + expect(formatJsonData([])).toEqual(['[]']) + expect(formatJsonData({})).toEqual(['{}']) + }) + }) + + describe('compareJson (integration)', () => { + it('compares identical JSON text', () => { + const json = '{"a":1,"b":[1,2]}' + const result = compareJson(json, json) + expect(result.stats.same).toBeGreaterThan(0) + expect(result.left.length).toBe(result.right.length) + expect(result.left.every((l, i) => l.lineNumber === i + 1)).toBe(true) + }) + + it('throws on invalid JSON', () => { + expect(() => compareJson('{invalid', '{}')).toThrow('JSON解析失败') + expect(() => compareJson('{}', '[unclosed')).toThrow('JSON解析失败') + }) + + it('throws on empty invalid input', () => { + expect(() => compareJson('', '{}')).toThrow('JSON解析失败') + }) + + it('detects value change in JSON', () => { + const result = compareJson('{"name":"Alice"}', '{"name":"Bob"}') + expect(result.stats.modify).toBeGreaterThan(0) + }) + + it('respects ignoreListOrder option', () => { + const a = '{"items":[1,2,3]}' + const b = '{"items":[3,2,1]}' + const ordered = compareJson(a, b, false) + const ignored = compareJson(a, b, true) + expect(ignored.stats.same).toBeGreaterThanOrEqual(ordered.stats.same) + }) + + it('handles deeply nested JSON', () => { + const a = JSON.stringify({ level1: { level2: { level3: { value: 1 } } } }) + const b = JSON.stringify({ level1: { level2: { level3: { value: 2 } } } }) + const result = compareJson(a, b) + expect(result.stats.modify).toBeGreaterThan(0) + }) + + it('handles JSON with unicode', () => { + const a = '{"msg":"你好"}' + const b = '{"msg":"世界"}' + const result = compareJson(a, b) + expect(result.left.length).toBeGreaterThan(0) + }) + + it('handles boolean and null values', () => { + const a = '{"flag":true,"empty":null}' + const b = '{"flag":false,"empty":null}' + const result = compareJson(a, b) + expect(result.stats.modify).toBeGreaterThan(0) + }) + + it('handles array length mismatch', () => { + const result = compareJson('[1,2]', '[1,2,3]') + expect(result.stats.insert + result.stats.modify).toBeGreaterThan(0) + }) + + it('handles empty objects comparison', () => { + const result = compareJson('{}', '{}') + expect(result.left.length).toBe(result.right.length) + expect(result.left.every(l => l.type === 'same')).toBe(true) + }) + + it('handles whitespace in JSON parse (valid JSON)', () => { + const result = compareJson(' { "a" : 1 } ', '{ "a" : 1 }') + expect(result.stats.same).toBeGreaterThan(0) + }) + }) +}) diff --git a/tests/unit/comparator/textDiff.test.js b/tests/unit/comparator/textDiff.test.js new file mode 100644 index 0000000..c83e3e3 --- /dev/null +++ b/tests/unit/comparator/textDiff.test.js @@ -0,0 +1,195 @@ +import { describe, it, expect } from 'vitest' +import { + computeDiff, + shouldMergeAsModify, + compareTextByLine, + compareTextByChar, +} from '../../../src/utils/comparator/textDiff.js' + +describe('comparator/textDiff', () => { + describe('shouldMergeAsModify', () => { + it('returns false for identical lines', () => { + expect(shouldMergeAsModify('same', 'same')).toBe(false) + }) + + it('merges JSON key-value lines with same key', () => { + expect(shouldMergeAsModify(' "name": "Alice"', ' "name": "Bob"')).toBe(true) + }) + + it('does not merge JSON key-value lines with different keys', () => { + expect(shouldMergeAsModify(' "name": "Alice"', ' "age": 30')).toBe(false) + }) + + it('merges structurally similar lines (>50% prefix)', () => { + expect(shouldMergeAsModify(' { "a": 1 }', ' { "a": 2 }')).toBe(true) + }) + + it('does not merge completely different lines', () => { + expect(shouldMergeAsModify('foo', 'bar')).toBe(false) + }) + }) + + describe('computeDiff', () => { + it('finds full match path', () => { + const path = computeDiff(['a', 'b'], ['a', 'b']) + expect(path.some(p => p.x === 0 && p.y === 0)).toBe(true) + expect(path.some(p => p.x === 1 && p.y === 1)).toBe(true) + }) + + it('prefers leftmost match for duplicate chars in B (hello vs helloworld)', () => { + const path = computeDiff('hello'.split(''), 'helloworld'.split('')) + const oMatch = path.find(p => { + const charA = 'hello'[p.x] + const charB = 'helloworld'[p.y] + return charA === 'o' && charB === 'o' + }) + expect(oMatch).toBeDefined() + expect(oMatch.y).toBe(4) + }) + + it('handles empty arrays', () => { + const path = computeDiff([], []) + expect(path).toEqual([{ x: 0, y: 0 }]) + }) + + it('handles one empty array', () => { + const path = computeDiff(['a'], []) + expect(path[path.length - 1]).toEqual({ x: 1, y: 0 }) + }) + }) + + describe('compareTextByLine', () => { + it('marks identical multi-line text as all same', () => { + const result = compareTextByLine('line1\nline2\nline3', 'line1\nline2\nline3') + expect(result.left.every(l => l.type === 'same')).toBe(true) + expect(result.right.every(l => l.type === 'same')).toBe(true) + expect(result.stats).toEqual({ same: 3, insert: 0, delete: 0, modify: 0 }) + }) + + it('handles empty strings (single empty line)', () => { + const result = compareTextByLine('', '') + expect(result.stats.same).toBe(1) + }) + + it('detects pure insertion on right side', () => { + const result = compareTextByLine('a', 'a\nb') + expect(result.stats.insert).toBeGreaterThan(0) + const insertLine = result.right.find(l => l.type === 'insert') + expect(insertLine?.content).toBe('b') + }) + + it('detects pure deletion on right side', () => { + const result = compareTextByLine('a\nb', 'a') + expect(result.stats.delete).toBeGreaterThan(0) + const deleteLine = result.left.find(l => l.type === 'delete') + expect(deleteLine?.content).toBe('b') + }) + + it('merges similar JSON lines as modify', () => { + const result = compareTextByLine( + ' "name": "Alice"', + ' "name": "Bob"' + ) + expect(result.stats.modify).toBe(1) + expect(result.left[0].type).toBe('modify') + expect(result.right[0].type).toBe('modify') + }) + + it('shows delete+insert for different JSON keys', () => { + const result = compareTextByLine( + ' "name": "Alice"', + ' "age": 30' + ) + expect(result.stats.delete).toBe(1) + expect(result.stats.insert).toBe(1) + expect(result.stats.modify).toBe(0) + }) + + it('assigns line numbers correctly for same lines', () => { + const result = compareTextByLine('a\nb', 'a\nb') + expect(result.left[0].lineNumber).toBe(1) + expect(result.left[1].lineNumber).toBe(2) + }) + + it('sets null lineNumber for placeholder lines on opposite side', () => { + const result = compareTextByLine('only-left', 'only-right') + const leftInsert = result.left.find(l => l.type === 'insert') + const rightDelete = result.right.find(l => l.type === 'delete') + if (leftInsert) expect(leftInsert.lineNumber).toBeNull() + if (rightDelete) expect(rightDelete.lineNumber).toBeNull() + }) + + it('handles completely different single lines', () => { + const result = compareTextByLine('aaa', 'bbb') + expect(result.stats.delete + result.stats.insert + result.stats.modify).toBeGreaterThan(0) + }) + + it('handles trailing newline difference consistently', () => { + const result = compareTextByLine('a\n', 'a') + expect(result.left.length).toBe(result.right.length) + }) + }) + + describe('compareTextByChar', () => { + it('marks identical lines as same with char count in stats', () => { + const result = compareTextByChar('hello', 'hello') + expect(result.left[0].type).toBe('same') + expect(result.stats.same).toBe(5) + }) + + it('highlights character differences inline', () => { + const result = compareTextByChar('abc', 'adc') + expect(result.left[0].inlineHighlight).toBe(true) + expect(result.left[0].html).toContain('diff-highlight') + }) + + it('handles empty vs non-empty line', () => { + const result = compareTextByChar('', 'hello') + expect(result.stats.insert).toBeGreaterThan(0) + }) + + it('handles insertion at end (hello vs helloworld)', () => { + const result = compareTextByChar('hello', 'helloworld') + expect(result.stats.insert).toBeGreaterThan(0) + expect(result.left[0].html).toContain('hello') + }) + + it('handles deletion (helloworld vs hello)', () => { + const result = compareTextByChar('helloworld', 'hello') + expect(result.stats.delete).toBeGreaterThan(0) + }) + + it('handles modify at start (na vs aa)', () => { + const result = compareTextByChar('na', 'aa') + expect(result.stats.delete).toBeGreaterThan(0) + expect(result.stats.insert).toBeGreaterThan(0) + expect(result.left[0].inlineHighlight).toBe(true) + }) + + it('aligns lines by index across multiline text', () => { + const result = compareTextByChar('a\nb', 'a\nc') + expect(result.left).toHaveLength(2) + expect(result.right).toHaveLength(2) + expect(result.left[0].type).toBe('same') + expect(result.left[1].type).toBe('modify') + }) + + it('handles extra lines on one side', () => { + const result = compareTextByChar('a', 'a\nb') + expect(result.left.length).toBe(2) + expect(result.left[1].content).toBe('') + expect(result.right[1].content).toBe('b') + }) + + it('escapes HTML in diff output', () => { + const result = compareTextByChar('