From f1502ed08c1ad7bca1732023bad5d8bfea2bca91 Mon Sep 17 00:00:00 2001 From: renjue Date: Thu, 25 Jun 2026 11:15:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20ToolBox=20?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E8=80=85=E5=B7=A5=E5=85=B7=E7=AE=B1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .dockerignore | 13 + .env.example | 11 + .gitea/README.md | 182 + .gitea/act-runner/.env.example | 3 + .gitea/act-runner/README.md | 183 + .gitea/act-runner/config.yaml | 33 + .gitea/act-runner/docker-compose.yml | 22 + .gitea/workflows/ci.yml | 263 ++ .gitignore | 30 + .npmrc | 2 + CHANGELOG.md | 26 + CONTRIBUTING.md | 47 + Dockerfile | 65 + LICENSE | 21 + NOTICE | 19 + README.md | 139 + SECURITY.md | 32 + deploy.sh | 66 + deploy/docker-entrypoint.sh | 36 + deploy/nginx.conf | 21 + index.html | 14 + package-lock.json | 3945 +++++++++++++++++++++ package.json | 40 + public/config.js | 1 + src/App.vue | 146 + src/components/DateTimePicker.vue | 818 +++++ src/components/JsonTreeNode.vue | 396 +++ src/composables/useLineNumberEditor.js | 51 + src/config/site.js | 62 + src/main.js | 12 + src/router/index.js | 82 + src/style.css | 19 + src/styles/line-number-editor.css | 44 + src/utils/analytics.js | 18 + src/utils/byteUtils.js | 10 + src/utils/color.js | 87 + src/utils/comparator/html.js | 31 + src/utils/comparator/index.js | 3 + src/utils/comparator/jsonCompare.js | 771 ++++ src/utils/comparator/textDiff.js | 375 ++ src/utils/encoder.js | 117 + src/utils/jsonFormatter/jsonPath.js | 142 + src/utils/timestamp.js | 74 + src/utils/variableName.js | 85 + src/views/ColorConverter.vue | 1482 ++++++++ src/views/Comparator.vue | 1472 ++++++++ src/views/EncoderDecoder.vue | 1172 ++++++ src/views/Home.vue | 201 ++ src/views/JsonFormatter.vue | 1953 ++++++++++ src/views/QRCodeGenerator.vue | 700 ++++ src/views/TimestampConverter.vue | 913 +++++ src/views/VariableNameConverter.vue | 526 +++ tests/unit/byteUtils.test.js | 30 + tests/unit/color.test.js | 42 + tests/unit/comparator/html.test.js | 41 + tests/unit/comparator/jsonCompare.test.js | 243 ++ tests/unit/comparator/textDiff.test.js | 195 + tests/unit/encoder.test.js | 77 + tests/unit/jsonPath.test.js | 89 + tests/unit/router.test.js | 22 + tests/unit/site.test.js | 63 + tests/unit/timestamp.test.js | 111 + tests/unit/useLineNumberEditor.test.js | 19 + tests/unit/variableName.test.js | 72 + vite.config.js | 26 + vitest.config.js | 14 + 66 files changed, 18020 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/README.md create mode 100644 .gitea/act-runner/.env.example create mode 100644 .gitea/act-runner/README.md create mode 100644 .gitea/act-runner/config.yaml create mode 100644 .gitea/act-runner/docker-compose.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100755 deploy.sh create mode 100755 deploy/docker-entrypoint.sh create mode 100644 deploy/nginx.conf create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/config.js create mode 100644 src/App.vue create mode 100644 src/components/DateTimePicker.vue create mode 100644 src/components/JsonTreeNode.vue create mode 100644 src/composables/useLineNumberEditor.js create mode 100644 src/config/site.js create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/style.css create mode 100644 src/styles/line-number-editor.css create mode 100644 src/utils/analytics.js create mode 100644 src/utils/byteUtils.js create mode 100644 src/utils/color.js create mode 100644 src/utils/comparator/html.js create mode 100644 src/utils/comparator/index.js create mode 100644 src/utils/comparator/jsonCompare.js create mode 100644 src/utils/comparator/textDiff.js create mode 100644 src/utils/encoder.js create mode 100644 src/utils/jsonFormatter/jsonPath.js create mode 100644 src/utils/timestamp.js create mode 100644 src/utils/variableName.js create mode 100644 src/views/ColorConverter.vue create mode 100644 src/views/Comparator.vue create mode 100644 src/views/EncoderDecoder.vue create mode 100644 src/views/Home.vue create mode 100644 src/views/JsonFormatter.vue create mode 100644 src/views/QRCodeGenerator.vue create mode 100644 src/views/TimestampConverter.vue create mode 100644 src/views/VariableNameConverter.vue create mode 100644 tests/unit/byteUtils.test.js create mode 100644 tests/unit/color.test.js create mode 100644 tests/unit/comparator/html.test.js create mode 100644 tests/unit/comparator/jsonCompare.test.js create mode 100644 tests/unit/comparator/textDiff.test.js create mode 100644 tests/unit/encoder.test.js create mode 100644 tests/unit/jsonPath.test.js create mode 100644 tests/unit/router.test.js create mode 100644 tests/unit/site.test.js create mode 100644 tests/unit/timestamp.test.js create mode 100644 tests/unit/useLineNumberEditor.test.js create mode 100644 tests/unit/variableName.test.js create mode 100644 vite.config.js create mode 100644 vitest.config.js 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('