commit 72352657491d4c60d535402d91917de060adc638 Author: fanhongcai Date: Sat Aug 15 15:27:02 2026 +0800 init: F9智慧缫丝系统 - 后端(server) + 主前端(web) + 打印设计器(openprint) 首次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5f8403 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# ===== 敏感文件 ===== +token.txt +*.token +zk_out.json + +# ===== 运行时缓存 ===== +f9mes_cache.db +*.db + +# ===== 本地工具 / 临时 ===== +.playwright-cli/ + +# ===== Node ===== +node_modules/ +dist/ +dist-ssr/ +*.local + +# ===== .NET ===== +bin/ +obj/ +*.user + +# ===== 日志 ===== +*.log +logs/ + +# ===== 系统 ===== +.DS_Store +Thumbs.db +.idea/ +.vscode/ diff --git a/AI开发日志/2026-08-15.md b/AI开发日志/2026-08-15.md new file mode 100644 index 0000000..8240959 --- /dev/null +++ b/AI开发日志/2026-08-15.md @@ -0,0 +1,135 @@ +# F9 智慧缫丝系统 —— CodeBuddy 持续工作存档 + +> 本文件为 CodeBuddy 开发日志归档(每日一份),归档日期:2026-08-15。 +> 新对话开始后:先读本文件 + `功能清单` + `项目说明`,按「任务状态」继续推进,无需重新调研。 + +最后更新:2026-08-15 + +--- + +## 一、任务状态总览(按用户约定的优先级 1→2→3) + +| 序号 | 任务 | 状态 | 说明 | +|---|---|---|---| +| 1 | 通用打印接入主前端 | 🔄 **进行中(方案已定,代码未写)** | 三步:后端取行数据接口 → openprint 外部打印模式 → web 打印按钮 | +| 2 | 简易工作流(采购/领料审批闭环) | ⏳ 待做 | 4 张表已有,缺服务+API+前端审批 UI | +| 3 | 专业业务逻辑(自动定级/能耗分摊/成本盈亏/断料预警) | ⏳ 待做 | 依赖任务 1/2 部分能力 | + +已完成的里程碑: +- ✅ 动态 CRUD 体系(后端 EntityCatalog + CrudService + DataController,前端 CrudPage + table-map 配置驱动) +- ✅ openprint 打印设计器全链路(模板设计→数据源内省→预览/打印,推 PDF 到本地客户端) +- ✅ 工作台接真实数据(WorkBenchController + workbench/index.vue 重写) + +--- + +## 二、项目架构速查 + +### 2.1 三端结构 +| 端 | 目录 | 技术栈 | 端口 | 说明 | +|---|---|---|---|---| +| PC 前端 | `web/` | Vue3 + Element Plus + ECharts + Pinia | vite dev 5173 | 代理 `/api` → localhost:5136 | +| 后端 | `server/` | .NET 10 + FreeSql (v3.5.311) | 5136 | MySQL 云库 `f9web`(116.198.221.105) | +| 打印设计器 | `openprint/` | Vue3 + Naive UI + Vite | 5227 | 对接 F9 的 `PrintApiController`,推 PDF(base64) 到本地客户端 `127.0.0.1:18888` | +| 电视大屏 / 小程序 | — | — | — | 未开始(按项目说明先完成 PC 端) | + +### 2.2 后端关键约定 +- **表命名**:模块前缀 + 下划线,如 `CRM_Customer`、`RawMaterial_Instock`、`Common_Workflow`、`RepCenter_ReportDesign`;实体类同前缀,如 `RawMaterial_Instock`。 +- **默认字段**:`id`、`flag`(0=删除,>0 有效,<0 无效,-1=作废)、`addtime`、`adder`、`updatetime`、`updater`、`memo`。 +- **序列化**:`Program.cs` 全局 CamelCase + 忽略 null(即 C# 属性 `OrderNo` → JSON `orderNo`)。 +- **响应信封**:普通 API 用 `ApiResult`;**`PrintApiController`(`api/print`)返回裸 JSON** `{items,total}`(openprint 的 http-client 已按此解析,主前端 axios 也可透传)。 +- **动态查询**:`DataController` 用 `EntityCatalog`(表名→实体 Type 反射映射)+ `CrudService` 泛型反射调用,`GetList/GetById/Add/Update/Delete` 等通用接口。 +- **工作流表**(已有,未做服务):`Common_Workflow`(流程定义)/ `Common_WorkflowNode`(节点)/ `Common_WorkflowInstance`(实例)/ `Common_WorkflowTask`(任务,`Status==0` 待办、`UserId` 当前处理人)。 + +### 2.3 web 前端关键约定 +- **Token 存储**:localStorage key = **`f9mes_token`**(`web/src/stores/user.js` 的 `TOKEN_KEY`)。 +- **API 封装**:`web/src/api/request.js`(axios 拦截器注入 Bearer);`web/src/api/index.js` 集中导出(含已新增的 `getWorkBenchSummary`)。 +- **配置驱动 CRUD**:`web/src/config/table-map.js` 定义每张表的列/表单配置,`web/src/components/CrudPage.vue` 是通用页面组件,菜单由 `InitDataService` 生成。 + +### 2.4 openprint 关键约定 +- **对接开关**:`openprint/src/main.ts` 仅当配置 `VITE_OPENPRINT_API_BASE` 时才切换云端仓库(HttpRepository/HttpDataSource);`.env` 中配 `VITE_OPENPRINT_API_BASE` + `VITE_OPENPRINT_API_TOKEN`。 +- **HttpClient token**:构造时静态传入(`http-client.ts` 的 `HttpOptions.token`),**构造后不可变**;若改运行时 token 需改构造时机或加 setter。 +- **绑定 Path 约定**:后端 `PrintService.BuildFields` 生成 `Path = "{TableId}.{PropName}"`(如 `RawMaterial_Instock.OrderNo`);openprint 的 `expression.ts`/`data-binder.ts` 按路径取值。**注意**:序列化后 JSON 为 camelCase,而 Path 是 PascalCase,取值器需兼容(大小写不敏感取 key 或按 camelCase 注入)。 +- **打印出口**:`PrintDialog.vue` 的 `doPrint()`:`store.buildTemplate()` + `buildPreviewData(dsStore.activeFields)` → `buildPrintPayload({template, data})` → 推本地客户端(POST `/print`,`127.0.0.1:18888`)。**当前用假数据**,是接入真实数据的关键改造点。 +- **数据源内省**:`PrintApiController` 的 `data-sources`/`fields` 由 `PrintService` 按实体类前缀分组(ModuleNames 字典),字段缓存 TTL 10 分钟。 + +--- + +## 三、任务 1:通用打印接入主前端(进行中) + +### 3.1 目标 +表单通用打印:主前端任意 CRUD 列表 → 选中一行 → 选打印模板 → 打开 openprint 用**该行真实数据**打印(针式/A4 均支持,设计器已具备)。 + +### 3.2 已锁定的三步方案(代码未写,按序实施) + +**Step A — 后端:取行数据接口** +- 位置:`PrintApiController.cs` 新增(或新 controller),路由建议 `GET api/print/data/{table}/{id}`。 +- 实现:基于 `EntityCatalog` 动态取实体 Type → FreeSql 按 `id` 查询单行 → 输出结构匹配绑定 Path:`{ "": { "": value, ... } }`(如 `{ "rawMaterial_Instock": { "orderNo": "...", ... } }` 或 PascalCase 表名,需与 PrintService.BuildFields 生成的 Path 前缀一致,建议直接复用 BuildFields 的 TableId)。 +- 返回裸 JSON(同 PrintApiController 风格,走 openprint http-client 的 json() 直接拿对象)。 + +**Step B — openprint:外部打印模式** +- 解析 URL 参数:`?print=1&template=&table=&row=&token=`。 +- 启动流程(main.ts 或 App.vue):检测 `print=1` 时 → 用 URL 的 token(或 localStorage)替换 HttpClient 的 token → 加载模板(designer store `loadTemplate`)→ 调 `api/print/data/{table}/{row}` 拉真实数据 → 注入到 dataSource store 供 `doPrint` 使用(如新增 `externalData` 状态,`PrintDialog.doPrint()` 优先用它替代 `buildPreviewData`)→ 自动打开打印弹窗。 +- 注意:`designer.ts` 有 `loadTemplate({id,name,data})`/`buildTemplate()`/`setRepository()`/`restoreLastTemplate()`;`dataSource.ts` 的 `activeFields` 决定字段列表;只读模式下应隐藏设计工具栏(TopToolbar 需加 `readonly`/`external` 模式判断)。 +- token:主前端 web 存于 localStorage `f9mes_token`,openprint 与 web 同源部署时可直接读;跨端口时用 URL 传 token 更稳(注意 URL 长度与泄露,内网 MES 可接受)。 + +**Step C — web:CrudPage 打印按钮** +- `CrudPage.vue` 工具栏加「打印」按钮(选中一行时可用)→ 弹窗列出该表可用模板(调 `api/print/templates` 按 `tableId` 过滤)→ 确认后 `window.open` openprint 打印 URL:`http://localhost:5227/?print=1&template={id}&table={tableName}&row={rowId}&token={token}`。 +- 需要后端提供"按表过滤模板"的能力(`PrintApiController.templates` 若支持 tableId 查询则直接用,否则加参数)。 + +### 3.3 关键文件清单 +- 后端:`server/src/F9MES.Api/Controllers/PrintApiController.cs`、`server/src/F9MES.Application/Print/PrintService.cs`、`server/src/F9MES.Api/Common/EntityCatalog.cs`、`server/src/F9MES.Api/Controllers/DataController.cs` +- openprint:`src/main.ts`、`src/App.vue`、`src/config/backend.ts`、`src/repository/http-client.ts`(token 构造点)、`src/design/stores/designer.ts`(loadTemplate)、`src/design/stores/dataSource.ts`、`src/design/modals/PrintDialog.vue`(doPrint)、`src/design/toolbar/TopToolbar.vue`(showPrint 挂载)、`src/design/preview/preview-data.ts`(buildPreviewData)、`src/core/layout-engine/expression.ts` + `data-binder.ts`(取值) +- web:`src/components/CrudPage.vue`、`src/api/index.js`、`src/config/table-map.js`、`src/stores/user.js`(token) + +--- + +## 四、任务 2:简易工作流(待做) + +- 现状:4 张表(Common_Workflow/Node/Instance/Task)已有,`WorkBenchController` 已统计 `Common_WorkflowTask`(Status==0 && UserId==当前用户)为待办。 +- 要做: + 1. 后端:工作流服务(流程定义 CRUD、节点配置、发起实例、提交/审批/驳回/撤回流转)、API(建议 `api/workflow/...`)。 + 2. 前端:审批中心页面(我的待办、我发起的、流程配置);在采购单、领料申请等表单接审批。 + 3. 关联:任务 1 打印对审批单据同样适用。 + +--- + +## 五、任务 3:专业业务逻辑(待做) + +按优先级从高到低建议: +1. **断料预警**(原料库存超期/不足,工作台预警列表已预留位置) +2. **自动定级**(Lims 检验数据 → 调用定级标准自动判定生丝等级,标准表需先建/已有则复用) +3. **能耗分摊**(Enms 抄表 → 按投料量/产量/机台时长分摊到庄口 → 推成本) +4. **成本盈亏**(BizBoss 生产成本分摊 + 分庄口盈亏统计) + +--- + +## 六、工程实践与坑位(必读) + +1. **编译锁定**:后端进程运行时 DLL 被锁,`dotnet build` 会报 MSB3027/MSB3021 → 用 `dotnet build -o build_tmp` 验证编译,完成后删临时目录;**改动后端必须重启服务才生效**。 +2. **FreeSql 陷阱**:非空 DateTime 没有 `.HasValue`;空集合时 `.Where(x => ids.Contains(...))` 会生成非法 SQL,需先 `if (list.Count > 0)` 守卫。 +3. **JSON 大小写**:后端全局 camelCase,openprint 绑定 Path 是 PascalCase,取值需兼容。 +4. **openprint 是独立应用**:改动它用 `npm run dev`(端口 5227)独立调试;主前端 web 是 vite dev(5173)。 +5. 数据库连接(项目说明):MySQL `116.198.221.105` 库 `f9web` 用户 `f9web`;阿里云 OSS(bbit-f9-web);天气用 Open-Meteo。 +6. 项目要求:类/文件命名带模块前缀、各模块类放各自文件夹、公共类放 Common;界面 MES 风格、高可配置、表单必须支持打印、表单带简易工作流。 + +--- + +## 七、本轮对话已完成的工作(2026-08 中旬) + +### 7.1 工作台真实数据(已完成 ✅) +- 新增 `server/src/F9MES.Api/Controllers/WorkBenchController.cs`:`GET api/workbench/summary`,聚合统计卡(今日产丝=ProHou_Daily.OutputWeight 合计、在线机组=ProQian_Machine 启用且运行、待办=工单+工作流待办、预警数)+ 近 30 天趋势 + 庄口占比 + 待办列表(工单/审批)+ 预警列表。 +- `web/src/api/index.js`:新增 `getWorkBenchSummary`。 +- `web/src/views/workbench/index.vue`:重写,4 统计卡/趋势折线/庄口占比饼图/待办与预警列表全部接真实数据,无数据时占位,保留天气+快捷入口。 +- 已用 `-o build_tmp` 验证编译通过;**需重启后端生效**。 + +### 7.2 openprint 全链路调研(已完成,结论见第三节) +已通读:PrintApiController / PrintService / EntityCatalog / DataController / http-repo / http-datasource / http-client / backend.ts / main.ts / App.vue / TopToolbar / PrintDialog / designer store / dataSource store / preview-data / expression / data-binder / CrudPage / table-map / request.js / user store / 两端 vite 配置。结论已沉淀到第三节。 + +--- + +## 八、下一步行动(新对话从这里开始) + +1. 先读本文件 + `功能清单` + `项目说明`(若工作区未加载)。 +2. 按 **3.2 的 Step A → B → C** 依次实施任务 1(打印接入),每步完成后用 `dotnet build -o build_tmp` / `npm run build` 验证,openprint 与 web 可分别 `npm run dev` 联调。 +3. 完成后再推进任务 2(简易工作流)。 +4. 任何大改动前更新本文件「最后更新」日期与任务状态。 diff --git a/openprint/.gitignore b/openprint/.gitignore new file mode 100644 index 0000000..368517d --- /dev/null +++ b/openprint/.gitignore @@ -0,0 +1,41 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local +参考资料 +.workbuddy +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.vscode +*.tsbuildinfo +docs +.eslintcache +website + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/openprint/.prettierrc.json b/openprint/.prettierrc.json new file mode 100644 index 0000000..29a2402 --- /dev/null +++ b/openprint/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/openprint/README.md b/openprint/README.md new file mode 100644 index 0000000..e68d9fc --- /dev/null +++ b/openprint/README.md @@ -0,0 +1,477 @@ +

+ OpenPrint Logo +

+ +

OpenPrint

+ +

+ 开源 Web 打印设计器 · AI 驱动 · 跨平台 +

+ +

+ Stars + Release + License + Vue 3 + TypeScript + Fabric.js + npm +

+ +

+ 功能特性 · + 快速开始 · + AI 助手 · + 架构 · + 部署 · + FAQ +

+
+ +**OpenPrint** 是一款开源、零后端依赖的 Web 打印模板可视化设计器。它让用户通过拖拽即可设计快递面单、发票、标签、报表等打印模板,支持 AI 一句话生成、云打印、C++ 桌面客户端静默打印,3 分钟即可对接 ERP 系统。 + +> 纯前端架构,无需服务器,无需数据库,一个浏览器即可运行。 + +![设计器界面](screenshots/designer-overview.png) +*所见即所得的打印模板设计器,拖拽控件、实时预览* + +--- + +## 目录 + +- [核心亮点](#核心亮点) +- [功能特性](#功能特性) +- [快速开始](#快速开始) +- [AI 助手](#ai-助手) +- [技术架构](#技术架构) +- [项目结构](#项目结构) +- [部署](#部署) +- [桌面客户端](#桌面客户端) +- [ERP 对接](#erp-对接) +- [FAQ](#faq) +- [贡献指南](#贡献指南) +- [许可证](#许可证) + +--- + +## 核心亮点 + +### 🤖 AI 一句话生成模板 + +告别手动拖拽,直接描述需求即可生成专业打印模板。支持快递面单、物流标签、仓储单据、零售小票等多种场景。 + +![AI 助手](screenshots/ai-assistant.png) +*AI 自然语言生成打印模板,一句话完成面单设计* + +``` +用户输入:"帮我生成一张快递面单,含收件人、寄件人和运单号条码" +AI 输出: 自动创建模板 → 布局文本控件 → 添加条码 → 一键使用 +``` + +### ☁️ 云打印 + +跨地域多门店远程打印,模板统一管理。浏览器直接打印,免装驱动。云端打印队列,断线自动重试,权限管控,打印日志审计。 + +![云打印](screenshots/cloud-print.jpg) +*多门店远程打印,模板统一管理,打印队列监控* + +### 📊 Web 报表设计 + +分组表、交叉表、主从表等多级报表结构,支持求和/平均/计数等汇总函数,嵌入柱状图/折线图/饼图,条件格式高亮数据预警,多数据源拼接。 + +![报表设计](screenshots/report-design.png) +*分组报表、交叉表、图表,满足复杂数据展示需求* + +--- + +## 功能特性 + +### 设计器 + +| 功能 | 说明 | +|------|------| +| **控件库** | 文本、图片、矩形、圆形、线条、条码、二维码、表格、富文本、图表 10 种控件 | +| **拖拽设计** | 所见即所得,自由拖拽 + 吸附对齐 + 标尺辅助 | +| **多页支持** | 多页设计,每页独立页眉页脚,页间距可调 | +| **数据绑定** | 支持 JSON / CSV / API 数据源,Mustache 表达式绑定 | +| **主题切换** | 深色/浅色主题,一键切换 | +| **模板管理** | 本地存储,模板导入/导出,模板市场 | +| **实时预览** | 预览数据绑定效果,分页预览 | +| **导出** | PDF / JPG / SVG / HTML 多格式导出 | + +### 打印与对接 + +- **Web 直接打印** — 浏览器调用系统打印对话框 +- **桌面静默打印** — 配套 C++ Qt 客户端,零弹窗打印 +- **云打印** — 远程门店打印,打印队列管理 +- **ERP 对接** — HTTP API 对接,3 分钟接入 + +### 技术栈 + +![Vue 3](https://img.shields.io/badge/Vue_3-4FC08D?style=flat-square&logo=vue.js&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) +![Fabric.js](https://img.shields.io/badge/Fabric.js-7.x-blue?style=flat-square) +![Pinia](https://img.shields.io/badge/Pinia-FFD859?style=flat-square&logo=pinia&logoColor=black) +![Vite](https://img.shields.io/badge/Vite-646CFF?style=flat-square&logo=vite&logoColor=white) +![Vitest](https://img.shields.io/badge/Vitest-6E9F18?style=flat-square&logo=vitest&logoColor=white) +![Naive UI](https://img.shields.io/badge/Naive_UI-2E7D32?style=flat-square) +![UnoCSS](https://img.shields.io/badge/UnoCSS-333333?style=flat-square&logo=unocss&logoColor=white) + +--- + +## 快速开始 + +### npm 安装(推荐) + +```bash +# npm +npm i openprint26 + +# pnpm +pnpm add openprint26 + +# yarn +yarn add openprint26 +``` + +#### 在项目中使用 + +```ts +import { createApp } from 'vue' +import OpenPrint from 'openprint26' +import 'openprint26/dist/style.css' + +const app = createApp(App) +app.use(OpenPrint) +app.mount('#app') +``` + +```vue + +``` + +### 从源码运行 + +```bash +# 克隆仓库 +git clone https://gitee.com/haiming236/openprint.git +cd open-print + +# 安装依赖 +pnpm install + +# 启动开发服务器 +pnpm dev + +# 构建生产版本 +pnpm build + +# 预览生产构建 +pnpm preview +``` + +### 环境要求 + +- Node.js >= 22.18.0 +- pnpm >= 9.x + +### 在线演示 + +访问 [https://openprint.yy360.space](https://openprint.yy360.space) 在线体验设计器。 + +--- + +## AI 助手 + +OpenPrint 内置 AI 助手,支持自然语言生成打印模板。 + +### 使用方式 + +1. **新建模板** — 输入需求描述,AI 自动生成完整模板 +2. **修改当前模板** — 选中控件,让 AI 调整布局或样式 +3. **字段接地** — 自动识别数据字段并绑定到模板控件 + +### 技术原理 + +- 基于 LLM 解析用户意图,输出结构化模板描述 +- 内部规范引擎将描述转换为 Fabric.js 控件配置 +- 支持上下文感知,可在已有模板上增量修改 + +### 支持的场景 + +- 快递面单(顺丰、京东、邮政等格式) +- 物流标签(运单号、目的地、重量) +- 仓储单据(入库单、出库单、盘点单) +- 零售小票(收银小票、退换货单) +- 发票(增值税发票、收据) +- 自定义报表 + +--- + +## 技术架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 浏览器(Vue 3 SPA) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ 控制面板 │ │ 属性面板 │ │ 图层面板 │ │ 数据源面板 │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬────────┘ │ +│ └──────────────┴─────────────┴───────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Fabric.js Canvas │ │ +│ │ 文本 │ 图片 │ 条码 │ 二维码 │ 表格 │ 图表 │ 矩形 │ 圆形 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 排版引擎 (Layout Engine) │ │ +│ │ 数据绑定 │ 表达式计算 │ 分页引擎 │ 表格引擎 │ 分组引擎 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 渲染引擎 (Renderer) │ │ +│ │ HTML渲染 │ PDF导出 │ JPG导出 │ SVG导出 │ 头部渲染 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌────────┴────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ C++ Qt 客户端 │ │ 云打印服务 │ + │ 静默打印 │ │ 远程打印 │ + │ 本地打印机 │ │ 队列管理 │ + └──────────────┘ └──────────────┘ +``` + +### 核心模块 + +| 模块 | 路径 | 说明 | +|------|------|------| +| **设计器** | `src/design/` | Canvas 设计器、控件系统、面板、工具栏 | +| **排版引擎** | `src/core/layout-engine/` | 数据绑定、分页、表格、分组、表达式 | +| **渲染引擎** | `src/core/renderer-html/` | HTML 渲染、CSS 生成、富文本渲染 | +| **导出引擎** | `src/core/export-engine/` | PDF、JPG、SVG 导出 | +| **AI 助手** | `src/ai/` | AI 生成模板、上下文管理 | +| **控件库** | `src/design/canvas/controls/` | 10 种 Fabric.js 控件 | + +--- + +## 项目结构 + +``` +open-print/ +├── src/ +│ ├── ai/ # AI 助手(生成/规范化/客户端) +│ ├── core/ # 核心引擎 +│ │ ├── layout-engine/ # 排版引擎(数据绑定/分页/表格/分组) +│ │ ├── renderer-html/ # HTML 渲染器 +│ │ ├── export-engine/ # 导出引擎(PDF/JPG/SVG) +│ │ ├── chartkit/ # 图表渲染(柱状图/折线图/饼图) +│ │ ├── fonts/ # 字体加载与管理 +│ │ ├── headless/ # 无头渲染 +│ │ ├── spec/ # 模板规范与校验 +│ │ └── sdk/ # SDK 封装 +│ ├── design/ # 设计器界面 +│ │ ├── canvas/ # Canvas 画布 & 控件 +│ │ ├── panels/ # 属性/图层/数据源面板 +│ │ ├── preview/ # 预览面板 +│ │ ├── toolbar/ # 顶部工具栏 +│ │ ├── modals/ # 模态框(导入/导出/设置) +│ │ └── stores/ # Pinia 状态管理 +│ ├── repository/ # 数据仓库(本地/HTTP/模拟) +│ ├── types/ # TypeScript 类型定义 +│ ├── config/ # 配置(打印设置/AI 设置) +│ ├── theme/ # 主题系统 +│ └── utils/ # 工具函数 +├── website/ # 宣传官网 +├── dist/ # 构建输出 +├── public/ # 静态资源 +└── docs/ # 文档 +``` + +--- + +## 部署 + +### 静态部署 + +构建产物为纯静态文件,可部署到任何 HTTP 服务器: + +```bash +pnpm build +# 将 dist/ 目录部署到 Nginx / Apache / Vercel / Netlify 等 +``` + +### Nginx 配置示例 + +```nginx +server { + listen 80; + server_name your-domain.com; + root /path/to/open-print/dist; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +### Docker 部署 + +```dockerfile +FROM nginx:alpine +COPY dist/ /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +``` + +--- + +## 桌面客户端 + +OpenPrint 提供配套的 **C++ Qt 桌面打印客户端**,支持: + +- **静默打印** — 接收设计器模板,直接发送到本地打印机,零弹窗 +- **批量打印** — 支持 CSV/JSON 数据批量渲染与打印 +- **本地打印机管理** — 获取打印机列表、状态监控 +- **HTTP 接口** — 通过 REST API 接收打印任务 +- **跨平台** — Windows / Linux / macOS + +![桌面客户端](screenshots/desktop-client.jpg) +*C++ Qt 桌面客户端,静默打印,批量处理* + +### 技术栈 + +![C++](https://img.shields.io/badge/C++-17-00599C?style=flat-square&logo=cplusplus&logoColor=white) +![Qt](https://img.shields.io/badge/Qt-6.x-41CD52?style=flat-square&logo=qt&logoColor=white) +![CMake](https://img.shields.io/badge/CMake-064F8C?style=flat-square&logo=cmake&logoColor=white) + +--- + +## ERP 对接 + +OpenPrint 提供标准 HTTP API,3 分钟即可完成 ERP 对接: + +### 对接流程 + +1. **设计模板** — 在 Web 设计器中拖拽生成打印模板 +2. **导出模板文件** — 导出 `.json` 模板文件 +3. **API 对接** — ERP 系统通过 HTTP 推送数据 + 模板 ID,触发打印 + +![ERP 对接](screenshots/erp-integration.jpg) +*标准 HTTP API 对接,3 分钟接入 ERP 系统* + +### API 示例 + +```http +POST /api/print +Content-Type: application/json + +{ + "template_id": "waybill-001", + "data": { + "sender_name": "张三", + "sender_phone": "13800138000", + "receiver_name": "李四", + "receiver_address": "北京市朝阳区..." + }, + "printer": "local-printer-01", + "copies": 1 +} +``` + +--- + +## FAQ + +### 如何安装 OpenPrint? + +```bash +npm i openprint26 +``` + +详细用法见 [快速开始](#快速开始) 章节。 + +### OpenPrint 需要后端服务吗? + +不需要。OpenPrint 是纯前端应用,零后端依赖。模板存储在浏览器本地(IndexedDB),AI 功能通过直接调用 LLM API 实现。如需对接打印机或 ERP,可部署可选的桌面客户端。 + +### 支持哪些浏览器? + +Chrome / Edge / Firefox / Safari 等现代浏览器。推荐使用 Chromium 内核浏览器以获得最佳体验。 + +### 如何自定义字体? + +将字体文件(`.ttf` / `.woff2`)放入 `public/fonts/` 目录,并在 `src/core/fonts/catalog.ts` 中注册即可。 + +### 支持批量打印吗? + +支持。通过 CSV/JSON 数据源 + 模板引擎,可批量生成多页文档并导出或打印。桌面客户端支持批量静默打印。 + +### 如何导出模板与他人分享? + +使用模板导出功能导出 `.json` 文件,其他人可通过导入功能加载使用。也支持 URL 编码分享(零后端)。 + +--- + +## 贡献指南 + +欢迎贡献代码、提交 Issue 或建议! + +### 开发流程 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/amazing-feature`) +3. 提交更改 (`git commit -m 'feat: add amazing feature'`) +4. 推送到分支 (`git push origin feature/amazing-feature`) +5. 提交 Pull Request + +### 提交规范 + +本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +- `feat:` 新功能 +- `fix:` 修复 +- `docs:` 文档变更 +- `refactor:` 重构 +- `test:` 测试 +- `chore:` 构建/工具变更 + +### 开发命令 + +```bash +# 运行测试 +pnpm vitest + +# 类型检查 +pnpm type-check + +# 格式化代码 +pnpm format + +# 代码覆盖率 +pnpm vitest --coverage +``` + +--- + +## 许可证 + +[GPL3.0 License](LICENSE) + +Copyright (c) 2025 OpenPrint + +--- + +

+ OpenPrint — 让打印变得简单高效 +

+

+ GitHub · + Gitee · + 在线演示 +

diff --git a/openprint/env.d.ts b/openprint/env.d.ts new file mode 100644 index 0000000..1c055af --- /dev/null +++ b/openprint/env.d.ts @@ -0,0 +1,12 @@ +/// + +interface ImportMetaEnv { + /** 后端基地址,如 https://print.example.com;配置后设计器切云端仓库(§4) */ + readonly VITE_OPENPRINT_API_BASE?: string + /** 可选:Bearer 鉴权 token */ + readonly VITE_OPENPRINT_API_TOKEN?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/openprint/index.html b/openprint/index.html new file mode 100644 index 0000000..47809f8 --- /dev/null +++ b/openprint/index.html @@ -0,0 +1,13 @@ + + + + + + + OpenPrint 设计器 + + +
+ + + diff --git a/openprint/overview.md b/openprint/overview.md new file mode 100644 index 0000000..975bd12 --- /dev/null +++ b/openprint/overview.md @@ -0,0 +1,140 @@ +# OpenPrint 图表三修:矢量 PDF + 饼图空白 + 标签对齐 + +> 承接上轮「原生 SVG 图表组件」(chartkit + PrintChart + ChartViewLayer + data-binder 接入 + ChartProps)。本轮修复/增强三件事。 + +## 1. PDF 图表走矢量(svg2pdf) +**之前**:整页 HTML 经 `foreignObject` 栅格化成 PNG 再 `addImage` → 图表是位图,放大发虚。 +**现在**:混合矢量导出(`src/core/export-engine/export-pdf.ts`) +- 每页先 `stripCharts(page)` 剥离 chart 控件 → 栅格化(文本/表格位图底图)→ `addImage` 作背景; +- 再遍历该页 chart 节点,用 `svg2pdf` 把其 SVG 以**矢量**注入 jsPDF 对应 mm 盒(`x/y/width/height`)。 +- **思源宋体矢量文本**:`/fonts/SourceHanSerifCN-Regular.ttf`(1.0M 子集)注册进 jsPDF 为 `SourceHanSerifCN`;chartkit 三个渲染器 SVG 根 `font-family` 改为 `'SourceHanSerifCN','PingFang SC',...` 优先。 +- **三重兜底**:图表旋转 → 整页退回全栅格化;单图 svg2pdf 失败 → 该图单独栅格化兜底;字体加载失败 → 整页退回全栅格化。非图表页行为不变。 + +## 2. 饼图中间空白(bug 修复) +**根因**:`chartkit/core.ts` 的 `arcPath` 实心饼画成 `M start A end Z`(圆弧+弦 = 圆缺段),楔形不到圆心 → 中间空白。 +**修复**:实心饼改为 `M cx cy L start A end Z`(真正到达圆心的楔形);环形保持外弧+内弧环形扇区;360° 整圆特例用两段半圆弧。新增回归测试:饼图 path 必须含圆心坐标。 + +## 3. 标签默认对齐(功能) +- `ChartOptions` 新增 `labelAlign?: 'left' | 'center' | 'right'`,**默认 `left`**(解决"下面的标签默认都居中")。 +- `chartkit/core.ts` 新增 `legendStartX` / `xAxisLabelPos` 两个 helper:`bar/line` 的 x 轴类目标签、`bar/line/pie` 的图例均按 `labelAlign` 对齐(图例居中/右对齐按单行总宽偏移)。 +- `ChartProps.vue` 外观区新增「标签对齐」下拉(左 / 中 / 右)。 + +## 验证结果 +- `vue-tsc --build`:**0 错误** +- `vitest`:**249 测全绿**(chartkit 11 + 原有 238) +- `vite build`:**成功**(`pdf` chunk 因并入 svg2pdf.js 由 400→487kB,属动态加载导出包,无害) +- `vite` dev server:HTTP 200 + +## ⚠️ 验证缺口(需在真实浏览器确认) +svg2pdf 的矢量渲染、中文 TTF 注入、旋转页回退逻辑**无法在本环境(happy-dom)验证**——`export-pdf.spec.ts` 只 mock 栅格化层以验证 jsPDF 装配。请用户在 Chromium 中实际导出含图表的 PDF,确认: +1. 图表在 PDF 中随缩放保持清晰(矢量); +2. 中文标签正常显示(非方块/空白); +3. 含旋转图表的页面正确回退为位图且不丢图。 + +--- + +# 导出/打印空白纸修复 + +## 问题 +用户反馈:导出 PDF、推送本地打印都是空白纸。 + +## 根因 +`src/core/renderer-html/css-generator.ts` 的 `tableCss()` 里有一段 CSS 注释包含未转义的 `` 字符串: + +```css +/* ---------- 表格样式预设(Excel 式快速切换;class 由渲染端挂在
上) ---------- */ +``` + +导出/打印时,整页 CSS 被塞进 SVG 的 `` 时抛出: + +``` +Opening and ending tag mismatch: table line 180 and style +``` + +整张 SVG 无法作为 `` 加载,`rasterize.ts` 的 `svgToCanvas` 触发超时/错误兜底,返回**白底空 canvas**,最终 PDF / 打印推送都是白纸。 + +## 修复 +将注释改为无尖括号的描述: + +```css +/* ---------- 表格样式预设(Excel 式快速切换;class 由渲染端挂在 table 元素上) ---------- */ +``` + +改动仅一行:`src/core/renderer-html/css-generator.ts`。 + +## 验证 +- 用真实 Chromium 无头复现修复前:`` 加载 SVG `onerror`,canvas/iframe 空白。 +- 修复后:真实 Chromium 截图显示小票内容完整;`pageToImageBlob` JPEG blob 约 77KB;`documentToPdf` 底层栅格化路径恢复。 +- `vue-tsc --build`:0 错误。 +- `vitest run`:253 passed(31 文件)。 + +--- + +# 导出/打印「模糊」修复 + +## 问题 +空白纸修好后,用户反馈:导出 PDF、本地打印都**模糊**,"之前的版本很清晰"。 + +## 根因 +`src/core/export-engine/rasterize.ts` 的 `svgToCanvas` 走的是「**低分渲染 + drawImage 插值放大**」: + +1. `buildPageSvg` 生成的 SVG 宽高是 96dpi 像素值(58mm 小票仅 **220×454 px**); +2. `svgToCanvas` 把这个低分 SVG 渲染成位图 img,再 `drawImage` **放大**到 `×scale` 的 canvas。 + +`drawImage` 放大只是**像素插值**,不产生任何新细节 —— 文字在 220px 宽上光栅化后被拉伸,边缘发虚。这就是模糊的根源(JPEG 有损进一步放大这种感觉)。 + +> 关键认知:两种做法的 canvas 尺寸完全相同(都是 440×908),区别只在**内容来源**(插值 vs 原生)。所以单看尺寸测不出来,必须真实浏览器看文字边缘。 + +## 修复 +新增 `scaleSvgViewport()`:**加载前**把 SVG 根节点的 `width/height` 直接 `×scale`(`viewBox` 不动)。这样 foreignObject 内容以高分辨率**原生渲染**成位图,img 本身就是高分图;canvas 尺寸 = img 尺寸、`drawImage(img, 0, 0)` 不再缩放插值。 + +改动:`src/core/export-engine/rasterize.ts`(`svgToCanvas` + 新增 `scaleSvgViewport`)。 + +## scale 决策(保持不变 = 2) +对照 `scale=2`(76KB / 192dpi)vs `scale=3`(138KB / 288dpi):修复后两者文字都清晰、差异很小。故**保持 `scale=2` 默认**——清晰度足够且体积友好,不为提 scale 让体积反弹。需要更高清晰度时 `documentToPdf(result,{scale:3})` / 推送 `buildPrintPayload(req,{scale:3})` 可调。 + +## 验证 +- 真实 Chromium 对照:修复前文字边缘锯齿毛糙,修复后平滑锐利。 +- 真实导出 PDF(`documentToPdf` scale=2 jpeg)落盘 **81301 B**;`pdftoppm` 按 **300 DPI** 渲染后,小票标题/地址/电话/明细/合计全部清晰锐利(见 `临时/pdf-300dpi渲染.png`、`临时/export-test-清晰版.pdf`)。 +- `vue-tsc --build`:0 错误;`vitest run`:253 passed(31 文件)。 + +--- + +# 导出/打印清晰度策略:PNG 无损 + 默认最高清 + 移除清晰度选择器 + +## 问题 +模糊修复后,用户实测仍觉得不如之前清晰;复盘认为「底图从 PNG 切到 JPEG」是主因,要求: +- 换回 **PNG 无损底图**; +- 导出/打印默认**最高清**,不要再让用户选清晰度。 + +## 决策 +1. **底图默认从 JPEG 改回 PNG(无损)** + - `export-pdf.ts`、`export-engine/index.ts`、`print-client/payload.ts`、`rasterize.ts` 的默认 `imageType` 统一改为 `'png'`。 + - 思源宋体仍以 data-URI 内联;PNG 底图对中文/细线/条码边缘无 DCT 伪影。 +2. **默认 scale 从 2(192dpi)提到 3(288dpi·最高清)** + - PDF 导出、打印推送、底层 `pageToImageBlob` 的默认 scale 全部提到 `3`。 + - 程序/headless 仍可传 `scale: 2/4` 覆盖;只是不再暴露 UI 选择。 +3. **移除导出弹窗的「清晰度」选择器** + - `ExportDialog.vue` 删除 `scale` ref、`scaleOptions`、`NSelect` 行,以及 `onExport` 里的 `scale` 参数;用户不再面对清晰度选择。 + +## 文件改动 +- `src/core/export-engine/export-pdf.ts`:默认 `scale=3`、`imageType='png'`;注释更新。 +- `src/core/export-engine/index.ts`:默认 `scale=3`、`pdfImageType='png'`;注释更新。 +- `src/core/export-engine/rasterize.ts`:`pageToImageBlob` 默认 `scale=3`。 +- `src/core/print-client/payload.ts`:默认 `scale=3`、`imageType='png'`。 +- `src/core/print-client/client.ts`、`src/design/modals/PrintDialog.vue`:注释同步为 PNG 无损。 +- `src/design/toolbar/ExportDialog.vue`:移除「清晰度」选择器及 `NSelect` 导入。 + +## 验证 +- `vue-tsc --build`:**0 错误** +- `vitest run`:**253 passed(31 文件)** +- 真实 Chromium 导出: + - 收银小票(58mm)默认 PNG scale=3 导出 PDF:**2.57 MB**; + - 用 `pdftoppm -r 300` 打印级渲染后文字/条码全部锐利(见 `临时/receipt-300dpi-png-scale3.png`)。 + - 量化对比(同页同 scale=3):PNG 拉普拉斯高频能量 / JPEG ≈ **0.998**,说明清晰度的主要决定因素是前一轮的「高分原生渲染」修复;PNG 在此基础上彻底消除 JPEG 的 DCT 块伪影,与「最清晰」目标一致。 + +## 体积提示 +- PNG 无损清晰度最高,但体积显著大于 JPEG: + - 收银小票 PNG scale=3 ≈ 2.6 MB; + - 销售出库单/数据报表类 A4 页 PNG scale=3 可达 **20+ MB/页**。 +- 若本地打印客户端对 base64 载荷敏感,可在调用点显式传 `scale: 2` 或 `imageType: 'jpeg'`。 diff --git a/openprint/package-lock.json b/openprint/package-lock.json new file mode 100644 index 0000000..7d16c99 --- /dev/null +++ b/openprint/package-lock.json @@ -0,0 +1,9088 @@ +{ + "name": "openprint26", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openprint26", + "version": "0.0.0", + "dependencies": { + "@bwip-js/generic": "^4.11.2", + "@codemirror/lang-json": "^6.0.2", + "@tiptap/extension-font-family": "^3.29.2", + "@tiptap/extension-text-style": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2", + "@tiptap/vue-3": "^3.29.2", + "@vueuse/core": "^14.4.0", + "ajv": "^8.20.0", + "codemirror": "^6.0.2", + "dompurify": "^3.4.13", + "fabric": "^7.4.0", + "json-schema-to-ts": "^3.1.1", + "jspdf": "^4.2.1", + "mustache": "^4.2.0", + "opentype.js": "^2.0.0", + "pinia": "^4.0.2", + "qrcode": "^1.5.4", + "stylis": "^4.4.0", + "svg2pdf.js": "^2.7.0", + "vue": "^3.5.40", + "vue-codemirror6": "^1.6.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@iconify-json/carbon": "^1.2.25", + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.13.3", + "@types/qrcode": "^1.5.6", + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/coverage-v8": "^3.2.4", + "@vue/tsconfig": "^0.9.1", + "happy-dom": "^20.11.2", + "naive-ui": "^2.44.1", + "npm-run-all2": "^9.0.2", + "prettier": "3.9.5", + "typescript": "~6.0.0", + "unocss": "^66.7.5", + "vite": "^8.1.5", + "vite-plugin-vue-devtools": "^8.1.5", + "vitest": "^3.2.4", + "vue-tsc": "^3.3.7" + }, + "engines": { + "node": "^22.18.0 || >=24.12.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bwip-js/generic": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@bwip-js/generic/-/generic-4.11.2.tgz", + "integrity": "sha512-NSBGrja1Md/alQyGnJ96YJYvhxsqzV1ZhIxOsADvIo3s0PGYDISvxxcGDVwLXvWWU+38dcfFNFED47Owzjewzw==", + "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.8.tgz", + "integrity": "sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@css-render/plugin-bem": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz", + "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "css-render": "~0.15.14" + } + }, + "node_modules/@css-render/vue3-ssr": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz", + "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@iconify-json/carbon": { + "version": "1.2.25", + "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.25.tgz", + "integrity": "sha512-7GLgXnmi47Skh8DcPPiP8U3vgm3rOVziEe+flCdG/kwiVaeb649U3Eqt/ej4f3NvlZK8BrOxpR3xQlWFLZYblQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.131.0.tgz", + "integrity": "sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.131.0.tgz", + "integrity": "sha512-nlGIod6gw75x1aEDgLS+srj+JRGY0HHm9MI9YgzE/B64l6d6+H3MSP9NOgp0+HTg8tp4vV9rVfgQGgd+TfVZcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.131.0.tgz", + "integrity": "sha512-jukuV6xe5RbQKFo7QD34NDCLDZp4PSOm8rmckhNdH/60ymG5zXbDzGBEyc+nTkuLQNama2aSGCt+CPfpjNTqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.131.0.tgz", + "integrity": "sha512-g3JOo4khe9rslHm5WYaVDWb0HS/M1MLR3I9S8560MkKIcC96VQY00QjOlsuRyfSj/JDXj8i9T7ryPO2RidiXVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.131.0.tgz", + "integrity": "sha512-1hziITDTxjMePnX+dR9ocVT+EuZkQ8wm4FPAbmbEiKG+Phbo73J1ZnPAA6Y/aGsWF3McOFnQuZIktAFwalkfJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.131.0.tgz", + "integrity": "sha512-9uRxfXwyKG9+MwmGQBo2ncPNwZH5HTmCETFM2WiuDBNDCW4NC5ttSQkwCAMrTAWgwMzVBH1CP8pM0v7nebCWXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.131.0.tgz", + "integrity": "sha512-mgbLvzRShXOLBdWGInf08Af4q+pfj1xD8hSgLClDZ9of/BXkB6+LIhTH7fihiDUipqB3yoSkKBWaZ3Ejlf5Yag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.131.0.tgz", + "integrity": "sha512-OPT8++4aN6j2GJ8+3IZHS/byXoZP4aSBn+FoG6rgBJ2fKwPKXWF3MqrFMNW7NKHM28FLY579xYLxJSfgobEqPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.131.0.tgz", + "integrity": "sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.131.0.tgz", + "integrity": "sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.131.0.tgz", + "integrity": "sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.131.0.tgz", + "integrity": "sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.131.0.tgz", + "integrity": "sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.131.0.tgz", + "integrity": "sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.131.0.tgz", + "integrity": "sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.131.0.tgz", + "integrity": "sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.131.0.tgz", + "integrity": "sha512-3SkikPaEFoih1N83qLVEDLRLeY4nYsf6JT9SnWiMCQ5lGQdKup6bEuKCqkRiG9dD1IIaFeYz9RjlciPmYoFIWA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.131.0.tgz", + "integrity": "sha512-Os5bEhryeA2jkH+ZrnZyAC1EP5gs+X4YB1Fjqml7UPD5kU7ecsK1MPEVMfCrdt/GDNpDbavYXiOXOdyJ5b3OPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.131.0.tgz", + "integrity": "sha512-m+jNz9EuF0NXoiptc6B9h5yompZQVW/a5MJeOu5zojfH5yWk82tvF2ccrHkfhgtrS9h9DD5l1Qv8dWlfY7Nz8g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.131.0.tgz", + "integrity": "sha512-o14Hk8dAyiEUMFEWEgmAwFZvBt1RzAYLM3xeQ+5315JXgVYhoemivgYcbYVRbsFkS71ShMGlAFE0kPnr460rww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.131.0.tgz", + "integrity": "sha512-PgnWDfV0h+b16XNKbXU7Daib/BFSt/J2mEzfYIBu6JB/wNdlU+kVYXCkGA1A9fWkTbOgbjh4e6NhPeQOYvFhEA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "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.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "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.npmjs.org/@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.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tiptap/core": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.29.2.tgz", + "integrity": "sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.29.2.tgz", + "integrity": "sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.29.2.tgz", + "integrity": "sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.29.2.tgz", + "integrity": "sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.29.2.tgz", + "integrity": "sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.29.2.tgz", + "integrity": "sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.29.2.tgz", + "integrity": "sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.29.2.tgz", + "integrity": "sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.29.2.tgz", + "integrity": "sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.29.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.29.2.tgz", + "integrity": "sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-font-family": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.29.2.tgz", + "integrity": "sha512-HDq9+V8LbDFH1aQRH5Au0pmoDQ7bfa/C/k50a9mujzmA/PE7mLQkduUFfyDdiN4L60p7TVJAOJaN/loGC8PDxA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "3.29.2" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.29.2.tgz", + "integrity": "sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.29.2" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.29.2.tgz", + "integrity": "sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.29.2.tgz", + "integrity": "sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.29.2.tgz", + "integrity": "sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.29.2.tgz", + "integrity": "sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.29.2.tgz", + "integrity": "sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.29.2.tgz", + "integrity": "sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.29.2.tgz", + "integrity": "sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.29.2.tgz", + "integrity": "sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.29.2.tgz", + "integrity": "sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.29.2.tgz", + "integrity": "sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.29.2.tgz", + "integrity": "sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.29.2.tgz", + "integrity": "sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.29.2.tgz", + "integrity": "sha512-aQad9V9ROaEi3hE4g5z9wQ6lv5FSnzlnWFMVJxRjNlwXgm7H0fymxb4rGfca+pAwYkiRwpKYh1LatpJw2ECQAA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.29.2.tgz", + "integrity": "sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.29.2.tgz", + "integrity": "sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.29.2.tgz", + "integrity": "sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.29.2.tgz", + "integrity": "sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.29.2", + "@tiptap/extension-blockquote": "^3.29.2", + "@tiptap/extension-bold": "^3.29.2", + "@tiptap/extension-bullet-list": "^3.29.2", + "@tiptap/extension-code": "^3.29.2", + "@tiptap/extension-code-block": "^3.29.2", + "@tiptap/extension-document": "^3.29.2", + "@tiptap/extension-dropcursor": "^3.29.2", + "@tiptap/extension-gapcursor": "^3.29.2", + "@tiptap/extension-hard-break": "^3.29.2", + "@tiptap/extension-heading": "^3.29.2", + "@tiptap/extension-horizontal-rule": "^3.29.2", + "@tiptap/extension-italic": "^3.29.2", + "@tiptap/extension-link": "^3.29.2", + "@tiptap/extension-list": "^3.29.2", + "@tiptap/extension-list-item": "^3.29.2", + "@tiptap/extension-list-keymap": "^3.29.2", + "@tiptap/extension-ordered-list": "^3.29.2", + "@tiptap/extension-paragraph": "^3.29.2", + "@tiptap/extension-strike": "^3.29.2", + "@tiptap/extension-text": "^3.29.2", + "@tiptap/extension-underline": "^3.29.2", + "@tiptap/extensions": "^3.29.2", + "@tiptap/pm": "^3.29.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/vue-3": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.29.2.tgz", + "integrity": "sha512-rLX+V+HXbsVlk+My8mslvbnbVXZiQZHYtbOiRgxugsqZ9SK1mr1FpgZ/ZNcotJAjecT8FJmPa45ccDnVu66eGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.29.2", + "@tiptap/extension-floating-menu": "^3.29.2" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2", + "vue": "^3.0.0" + } + }, + "node_modules/@tsconfig/node24": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz", + "integrity": "sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@unocss/cli": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/cli/-/cli-66.7.5.tgz", + "integrity": "sha512-fgWkECRn2LGVo9sEpmjk/KJ3NSKUDitaZCNrJcEYM93DG+ELriTHcL+VWBlF4rcZf9fdGlq1nKbbRHUZirFCHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@unocss/config": "66.7.5", + "@unocss/core": "66.7.5", + "@unocss/preset-wind3": "66.7.5", + "@unocss/preset-wind4": "66.7.5", + "@unocss/transformer-directives": "66.7.5", + "cac": "^7.0.0", + "chokidar": "^5.0.0", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "tinyglobby": "^0.2.17", + "unplugin-utils": "^0.3.1" + }, + "bin": { + "unocss": "bin/unocss.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/config": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/config/-/config-66.7.5.tgz", + "integrity": "sha512-dkPl9glhEahJ+Xoja5ZseKKnH+vZaeaKQzg8b0otcKcPPNrHQgu1nu3QgfeOjnXOGrjZIotHwUeVtt4ZA2Skgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "unconfig": "^7.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/core": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/core/-/core-66.7.5.tgz", + "integrity": "sha512-UdJb8MiMywcau8QrWEVgUAz0kvoFHyR+sACwYCgmBh/BpKJGyR/zw/Ys3wvysbm0f+i/20VGBex/QQxYVlzdyQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/extractor-arbitrary-variants": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.7.5.tgz", + "integrity": "sha512-5zOkbnLIJc8E749qNjLXfPHl3FdHloop12h706/ojr6idK4ix0KLm43R//uLnphixCehdHJRuHnavnM4C/kQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/inspector": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/inspector/-/inspector-66.7.5.tgz", + "integrity": "sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/rule-utils": "66.7.5", + "colorette": "^2.0.20", + "gzip-size": "^6.0.0", + "sirv": "^3.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-attributify": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-attributify/-/preset-attributify-66.7.5.tgz", + "integrity": "sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-icons": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-icons/-/preset-icons-66.7.5.tgz", + "integrity": "sha512-ZsxadWnGGtHdANTikuMnjkQaT1qwUFJHZBF4fzVsPMnUiiKGs8rzXukwM9w3Gi9ontM7nbG2FYi9clWETSlpFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/utils": "^3.1.3", + "@unocss/core": "66.7.5", + "ofetch": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-mini": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-mini/-/preset-mini-66.7.5.tgz", + "integrity": "sha512-o37TSl4ecT0dKu+3/TYuTYht75h82SEwDNl476m9ve0KW4Pv1O2tT/9TWd7N5cutEpySr8/YtjMwtWBD+drxBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/extractor-arbitrary-variants": "66.7.5", + "@unocss/rule-utils": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-tagify": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-tagify/-/preset-tagify-66.7.5.tgz", + "integrity": "sha512-/8YB1tXVi+WmNKPhsCdtO1xnQKEkshSnWDp6AbvVP3f6qOF7adlMYpAt3kpWCoVUBsfOQ06ZQlnfricMjObyoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-typography": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-typography/-/preset-typography-66.7.5.tgz", + "integrity": "sha512-2dxC8LT9KYa29UZT4muDFl7DbIXvKktTfeSMbUGMdZKbHcuMtKSP2U52wti1PL5I9duqp4v+8G33EeVutGTUaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/rule-utils": "66.7.5" + } + }, + "node_modules/@unocss/preset-uno": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-uno/-/preset-uno-66.7.5.tgz", + "integrity": "sha512-zaUlYgNngbt50fZA/LbtsEnmPKojPqeiXCyUUiKQMv2uJQhncR32xhLZXVQ+TJD7hlfZ+6FAqlco1NIiAcub9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/preset-wind3": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-web-fonts": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-web-fonts/-/preset-web-fonts-66.7.5.tgz", + "integrity": "sha512-OLLTK7kswdSu51qxEm6O+AehecgCfS08Ivqo+280lKzFW7V1jXr5okeJ1ty+85oHCprJqzcD9iG1khxNRLomcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "ofetch": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind/-/preset-wind-66.7.5.tgz", + "integrity": "sha512-LVKgGr0A9Lc7F85xGXrrb71DDXMlHGA8bcj/Kht8BCsuRdBZadNbLlnv5qnSJiHYhi3/blzcgVoaeRor6L9yNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/preset-wind3": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind3": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind3/-/preset-wind3-66.7.5.tgz", + "integrity": "sha512-atFe/7Qein+oMdyZs0hEo+3EjV99Av2LVxDbzpCungxIfbS3TnQt70EIuHXMPNCVWLYwrMV+/P1n9Ntb0E3mow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/preset-mini": "66.7.5", + "@unocss/rule-utils": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind4": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind4/-/preset-wind4-66.7.5.tgz", + "integrity": "sha512-n3jIvQv8x1jXpmWfvNFBIqHNARki4mKZXfxAA31gekpXsRRTg16u1+yC1+PBBJgoEDFEnnmKnG7EZP88S8LVXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/extractor-arbitrary-variants": "66.7.5", + "@unocss/rule-utils": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/rule-utils": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/rule-utils/-/rule-utils-66.7.5.tgz", + "integrity": "sha512-/AKHBRF6ZOexE3EDDv8ZEYh40P5e2Eha41IYUbE1wjigW7++ssmvimSTdufJzZvU5DGP++E3B3WEsFPprZMjAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-attributify-jsx": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.7.5.tgz", + "integrity": "sha512-r8qDwNSt0eGSwWws3xsuIHb3PZYR2embFdYoE67hD/xlYgLjX1uPy/HUlGCSSh4uLc4vVYjurr0Oq5xF/B8YiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "oxc-parser": "^0.131.0", + "oxc-walker": "^0.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-compile-class": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-compile-class/-/transformer-compile-class-66.7.5.tgz", + "integrity": "sha512-KuECgsGF7tGe808vIvKViEjI0UCUgYgneJfK+/TWBkjC7s8dLVaUtJzzcP9/r6OfGlgyn/x1YTdRqTaCENa7Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-directives": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-directives/-/transformer-directives-66.7.5.tgz", + "integrity": "sha512-VMJApXXOwlDubkW+cNMmOkfxLefFupJr+yU23gGYUB2NPsuVltc8H5CDM0gfVebpeL5CUW05evxzEAk2wsju+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/rule-utils": "66.7.5", + "css-tree": "^3.2.1" + } + }, + "node_modules/@unocss/transformer-variant-group": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-variant-group/-/transformer-variant-group-66.7.5.tgz", + "integrity": "sha512-iDmP3mM8J+IxuQf5Uh33zsi528xnGxTpKRJ0c+LCrqBTTkR2tZr4aCzNmJ0g29Js2yEOsyNXRRc8BNTuvgn0AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/vite": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/vite/-/vite-66.7.5.tgz", + "integrity": "sha512-1z1TBCNJCR2WzjPtjzmCHr8rtzXHosMjLdsCNe6Mzsfwiw044gzzLVuiEZO9vIjkI50lvQ+gIOxOiVQG0hGAeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@unocss/config": "66.7.5", + "@unocss/core": "66.7.5", + "@unocss/inspector": "66.7.5", + "chokidar": "^5.0.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.17", + "unplugin-utils": "^0.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.2.1.tgz", + "integrity": "sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1", + "@vue/devtools-shared": "^8.2.1" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.9.tgz", + "integrity": "sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz", + "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/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": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/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/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/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/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/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/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/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/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/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/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-render": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" + } + }, + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT", + "optional": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.402", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", + "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/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/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fabric": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-7.4.0.tgz", + "integrity": "sha512-NalYDc3eifTl1C33zryQwpH6+XA/2ClxQrH9vkASkZw3tbkRmorpikhYMmxhUTmi7O3e9ODz0vOT8qfaCh9IVA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "canvas": "^3.2.0", + "jsdom": "^26.1.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/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/font-family-papandreou": { + "version": "0.2.0-patch2", + "resolved": "https://registry.npmjs.org/font-family-papandreou/-/font-family-papandreou-0.2.0-patch2.tgz", + "integrity": "sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/happy-dom": { + "version": "20.11.2", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.2.tgz", + "integrity": "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", + "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "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.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "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/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/magic-regexp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz", + "integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12", + "mlly": "^1.7.2", + "regexp-tree": "^0.1.27", + "type-level-regexp": "~0.1.17", + "ufo": "^1.5.4", + "unplugin": "^2.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/naive-ui": { + "version": "2.44.1", + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz", + "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.10", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.65" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, + "node_modules/npm-normalize-package-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", + "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.3.tgz", + "integrity": "sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^7.0.0", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^1.0.0", + "read-package-json-fast": "^6.0.0", + "shell-quote": "^1.8.4", + "which": "^7.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0", + "npm": ">= 10" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "license": "MIT", + "optional": true + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opentype.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-2.0.0.tgz", + "integrity": "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==", + "license": "MIT", + "bin": { + "ot": "bin/ot" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/oxc-parser": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.131.0.tgz", + "integrity": "sha512-SJ3/7ZPbgie8dr5Z9BI/M51zZbpXba+hRSG0MDzVwMW5CRQg2fjYE0jHGlLX4eeiibGgC/mzoDFKSDHwVZEHRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.131.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.131.0", + "@oxc-parser/binding-android-arm64": "0.131.0", + "@oxc-parser/binding-darwin-arm64": "0.131.0", + "@oxc-parser/binding-darwin-x64": "0.131.0", + "@oxc-parser/binding-freebsd-x64": "0.131.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.131.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.131.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.131.0", + "@oxc-parser/binding-linux-arm64-musl": "0.131.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.131.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.131.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.131.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.131.0", + "@oxc-parser/binding-linux-x64-gnu": "0.131.0", + "@oxc-parser/binding-linux-x64-musl": "0.131.0", + "@oxc-parser/binding-openharmony-arm64": "0.131.0", + "@oxc-parser/binding-wasm32-wasi": "0.131.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.131.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.131.0", + "@oxc-parser/binding-win32-x64-msvc": "0.131.0" + } + }, + "node_modules/oxc-walker": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-0.7.0.tgz", + "integrity": "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-regexp": "^0.10.0" + }, + "peerDependencies": { + "oxc-parser": ">=0.98.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz", + "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pinia": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz", + "integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==", + "license": "MIT", + "dependencies": { + "nostics": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/devtools-api": "^8.1.5", + "typescript": ">=5.6.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": false + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz", + "integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/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/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-package-json-fast": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", + "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.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.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "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": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT", + "optional": true + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "optional": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/seemly": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz", + "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "license": "MIT", + "bin": { + "specificity": "bin/specificity" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/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/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/svg2pdf.js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/svg2pdf.js/-/svg2pdf.js-2.7.0.tgz", + "integrity": "sha512-nXK4Wx28H0KtOktanm5nsphl1KMEoLNMelAT/776qxPAj9DshwYcqgdpKuBnY1nrcYOriQFHVQLE4tIag+aDJA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "font-family-papandreou": "^0.2.0-patch1", + "specificity": "^0.4.1", + "svgpath": "^2.3.0" + }, + "peerDependencies": { + "jspdf": "^4.0.0 || ^3.0.0 || ^2.0.0" + } + }, + "node_modules/svgpath": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.6.0.tgz", + "integrity": "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==", + "license": "MIT", + "funding": { + "url": "https://github.com/fontello/svg2ttf?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT", + "optional": true + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT", + "optional": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/treemate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz", + "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-level-regexp": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", + "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unconfig": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.5.0.tgz", + "integrity": "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "defu": "^6.1.4", + "jiti": "^2.6.1", + "quansync": "^1.0.0", + "unconfig-core": "7.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unocss": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/unocss/-/unocss-66.7.5.tgz", + "integrity": "sha512-nAdmU8TwnQoiLnQjZ6Hm1GmHy9lTexKsAZNEJtZnxN9wyFRc1eLjQgmh9r7UEGxBQMQLD8OZOgmk5HpwObbh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/cli": "66.7.5", + "@unocss/core": "66.7.5", + "@unocss/preset-attributify": "66.7.5", + "@unocss/preset-icons": "66.7.5", + "@unocss/preset-mini": "66.7.5", + "@unocss/preset-tagify": "66.7.5", + "@unocss/preset-typography": "66.7.5", + "@unocss/preset-uno": "66.7.5", + "@unocss/preset-web-fonts": "66.7.5", + "@unocss/preset-wind": "66.7.5", + "@unocss/preset-wind3": "66.7.5", + "@unocss/preset-wind4": "66.7.5", + "@unocss/transformer-attributify-jsx": "66.7.5", + "@unocss/transformer-compile-class": "66.7.5", + "@unocss/transformer-directives": "66.7.5", + "@unocss/transformer-variant-group": "66.7.5", + "@unocss/vite": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@unocss/astro": "66.7.5", + "@unocss/postcss": "66.7.5", + "@unocss/webpack": "66.7.5" + }, + "peerDependenciesMeta": { + "@unocss/astro": { + "optional": true + }, + "@unocss/postcss": { + "optional": true + }, + "@unocss/webpack": { + "optional": true + } + } + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", + "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vdirs": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz", + "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "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.4.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/vite-dev-rpc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-2.0.0.tgz", + "integrity": "sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^4.0.0", + "vite-hot-client": "^2.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-dev-rpc/node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vite-hot-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.2.0.tgz", + "integrity": "sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.4.1.tgz", + "integrity": "sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.0", + "error-stack-parser-es": "^1.0.5", + "obug": "^2.1.1", + "ohash": "^2.0.11", + "open": "^11.0.0", + "perfect-debounce": "^2.1.0", + "sirv": "^3.0.2", + "unplugin-utils": "^0.3.1", + "vite-dev-rpc": "^2.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.2.1.tgz", + "integrity": "sha512-5JLxXWWCo5lJMw16/xVeNvJ8k2zLwZPf1vITLzya/2IePrCBeGe/p/iAokgXHZpEi39fcYtPXmO8SaKeXmqCAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.2.1", + "@vue/devtools-kit": "^8.2.1", + "@vue/devtools-shared": "^8.2.1", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^6.0.0" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz", + "integrity": "sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vooks": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz", + "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-codemirror6": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.6.1.tgz", + "integrity": "sha512-0Ue6nWy055UKP/6LkT0I7nW5SRSFJ+3mokwBdHePQsj11T02mrg3A0jtHyBqH23nRHQFxd3NKw63sZJ1ygo5dA==", + "license": "MIT", + "dependencies": { + "vue-demi": "latest" + }, + "engines": { + "node": "^22.18.0 || >=24.12.0" + }, + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "codemirror": "^6.0.0", + "style-mod": "^4.0.0", + "vue": "^2.7.14 || ^3.3.4" + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.9.tgz", + "integrity": "sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.9" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vueuc": { + "version": "0.4.65", + "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz", + "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@css-render/vue3-ssr": "^0.15.10", + "@juggle/resize-observer": "^3.3.1", + "css-render": "^0.15.10", + "evtd": "^0.2.4", + "seemly": "^0.3.6", + "vdirs": "^0.1.4", + "vooks": "^0.2.4" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "optional": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT", + "optional": true + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/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/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/openprint/package.json b/openprint/package.json new file mode 100644 index 0000000..d32428c --- /dev/null +++ b/openprint/package.json @@ -0,0 +1,61 @@ +{ + "name": "openprint26", + "version": "0.0.0", + "private": false, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "format": "prettier --write --experimental-cli src/" + }, + "dependencies": { + "@bwip-js/generic": "^4.11.2", + "@codemirror/lang-json": "^6.0.2", + "@tiptap/extension-font-family": "^3.29.2", + "@tiptap/extension-text-style": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2", + "@tiptap/vue-3": "^3.29.2", + "@vueuse/core": "^14.4.0", + "ajv": "^8.20.0", + "codemirror": "^6.0.2", + "dompurify": "^3.4.13", + "fabric": "^7.4.0", + "json-schema-to-ts": "^3.1.1", + "jspdf": "^4.2.1", + "mustache": "^4.2.0", + "opentype.js": "^2.0.0", + "pinia": "^4.0.2", + "qrcode": "^1.5.4", + "stylis": "^4.4.0", + "svg2pdf.js": "^2.7.0", + "vue": "^3.5.40", + "vue-codemirror6": "^1.6.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@iconify-json/carbon": "^1.2.25", + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.13.3", + "@types/qrcode": "^1.5.6", + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/coverage-v8": "^3.2.4", + "@vue/tsconfig": "^0.9.1", + "happy-dom": "^20.11.2", + "naive-ui": "^2.44.1", + "npm-run-all2": "^9.0.2", + "prettier": "3.9.5", + "typescript": "~6.0.0", + "unocss": "^66.7.5", + "vite": "^8.1.5", + "vite-plugin-vue-devtools": "^8.1.5", + "vitest": "^3.2.4", + "vue-tsc": "^3.3.7" + }, + "engines": { + "node": "^22.18.0 || >=24.12.0" + } +} diff --git a/openprint/pnpm-lock.yaml b/openprint/pnpm-lock.yaml new file mode 100644 index 0000000..6c7f910 --- /dev/null +++ b/openprint/pnpm-lock.yaml @@ -0,0 +1,5852 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@bwip-js/generic': + specifier: ^4.11.2 + version: 4.11.2 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@tiptap/extension-font-family': + specifier: ^3.29.2 + version: 3.29.2(@tiptap/extension-text-style@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))) + '@tiptap/extension-text-style': + specifier: ^3.29.2 + version: 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/pm': + specifier: ^3.29.2 + version: 3.29.2 + '@tiptap/starter-kit': + specifier: ^3.29.2 + version: 3.29.2 + '@tiptap/vue-3': + specifier: ^3.29.2 + version: 3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)(vue@3.5.41(typescript@6.0.3)) + '@vueuse/core': + specifier: ^14.4.0 + version: 14.4.0(vue@3.5.41(typescript@6.0.3)) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + codemirror: + specifier: ^6.0.2 + version: 6.0.2 + dompurify: + specifier: ^3.4.13 + version: 3.4.13 + fabric: + specifier: ^7.4.0 + version: 7.4.0 + json-schema-to-ts: + specifier: ^3.1.1 + version: 3.1.1 + jspdf: + specifier: ^4.2.1 + version: 4.2.1 + mustache: + specifier: ^4.2.0 + version: 4.2.0 + opentype.js: + specifier: ^2.0.0 + version: 2.0.0 + pinia: + specifier: ^4.0.2 + version: 4.0.2(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + stylis: + specifier: ^4.4.0 + version: 4.4.0 + svg2pdf.js: + specifier: ^2.7.0 + version: 2.7.0(jspdf@4.2.1) + vue: + specifier: ^3.5.40 + version: 3.5.41(typescript@6.0.3) + vue-codemirror6: + specifier: ^1.6.1 + version: 1.6.1(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.8)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.41(typescript@6.0.3)) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + devDependencies: + '@iconify-json/carbon': + specifier: ^1.2.25 + version: 1.2.25 + '@tsconfig/node24': + specifier: ^24.0.4 + version: 24.0.4 + '@types/node': + specifier: ^24.13.3 + version: 24.13.3 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + '@vitejs/plugin-vue': + specifier: ^6.0.8 + version: 6.0.8(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.7(vitest@3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0)) + '@vue/tsconfig': + specifier: ^0.9.1 + version: 0.9.1(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)) + happy-dom: + specifier: ^20.11.2 + version: 20.11.2 + naive-ui: + specifier: ^2.44.1 + version: 2.44.1(vue@3.5.41(typescript@6.0.3)) + npm-run-all2: + specifier: ^9.0.2 + version: 9.0.3 + prettier: + specifier: 3.9.5 + version: 3.9.5 + typescript: + specifier: ~6.0.0 + version: 6.0.3 + unocss: + specifier: ^66.7.5 + version: 66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vite: + specifier: ^8.1.5 + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-plugin-vue-devtools: + specifier: ^8.1.5 + version: 8.2.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0) + vue-tsc: + specifier: ^3.3.7 + version: 3.3.9(typescript@6.0.3) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, tarball: https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, tarball: https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, tarball: https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==, tarball: https://registry.npmmirror.com/@babel/generator/-/generator-7.29.8.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==, tarball: https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, tarball: https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==, tarball: https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==, tarball: https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==, tarball: https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==, tarball: https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, tarball: https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==, tarball: https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==, tarball: https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==, tarball: https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==, tarball: https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, tarball: https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==, tarball: https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.7': + resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==, tarball: https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.29.7': + resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==, tarball: https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==, tarball: https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==, tarball: https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.8.tgz} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bwip-js/generic@4.11.2': + resolution: {integrity: sha512-NSBGrja1Md/alQyGnJ96YJYvhxsqzV1ZhIxOsADvIo3s0PGYDISvxxcGDVwLXvWWU+38dcfFNFED47Owzjewzw==, tarball: https://registry.npmmirror.com/@bwip-js/generic/-/generic-4.11.2.tgz} + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.8': + resolution: {integrity: sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==} + + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==, tarball: https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==, tarball: https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz} + peerDependencies: + vue: ^3.0.11 + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==, tarball: https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@iconify-json/carbon@1.2.25': + resolution: {integrity: sha512-7GLgXnmi47Skh8DcPPiP8U3vgm3rOVziEe+flCdG/kwiVaeb649U3Eqt/ej4f3NvlZK8BrOxpR3xQlWFLZYblQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, tarball: https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, tarball: https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==, tarball: https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + + '@oxc-parser/binding-android-arm-eabi@0.131.0': + resolution: {integrity: sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.131.0': + resolution: {integrity: sha512-nlGIod6gw75x1aEDgLS+srj+JRGY0HHm9MI9YgzE/B64l6d6+H3MSP9NOgp0+HTg8tp4vV9rVfgQGgd+TfVZcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.131.0': + resolution: {integrity: sha512-jukuV6xe5RbQKFo7QD34NDCLDZp4PSOm8rmckhNdH/60ymG5zXbDzGBEyc+nTkuLQNama2aSGCt+CPfpjNTqyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.131.0': + resolution: {integrity: sha512-g3JOo4khe9rslHm5WYaVDWb0HS/M1MLR3I9S8560MkKIcC96VQY00QjOlsuRyfSj/JDXj8i9T7ryPO2RidiXVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.131.0': + resolution: {integrity: sha512-1hziITDTxjMePnX+dR9ocVT+EuZkQ8wm4FPAbmbEiKG+Phbo73J1ZnPAA6Y/aGsWF3McOFnQuZIktAFwalkfJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.131.0': + resolution: {integrity: sha512-9uRxfXwyKG9+MwmGQBo2ncPNwZH5HTmCETFM2WiuDBNDCW4NC5ttSQkwCAMrTAWgwMzVBH1CP8pM0v7nebCWXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.131.0': + resolution: {integrity: sha512-mgbLvzRShXOLBdWGInf08Af4q+pfj1xD8hSgLClDZ9of/BXkB6+LIhTH7fihiDUipqB3yoSkKBWaZ3Ejlf5Yag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.131.0': + resolution: {integrity: sha512-OPT8++4aN6j2GJ8+3IZHS/byXoZP4aSBn+FoG6rgBJ2fKwPKXWF3MqrFMNW7NKHM28FLY579xYLxJSfgobEqPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.131.0': + resolution: {integrity: sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': + resolution: {integrity: sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': + resolution: {integrity: sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.131.0': + resolution: {integrity: sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.131.0': + resolution: {integrity: sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.131.0': + resolution: {integrity: sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.131.0': + resolution: {integrity: sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.131.0': + resolution: {integrity: sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.131.0': + resolution: {integrity: sha512-3SkikPaEFoih1N83qLVEDLRLeY4nYsf6JT9SnWiMCQ5lGQdKup6bEuKCqkRiG9dD1IIaFeYz9RjlciPmYoFIWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.131.0': + resolution: {integrity: sha512-Os5bEhryeA2jkH+ZrnZyAC1EP5gs+X4YB1Fjqml7UPD5kU7ecsK1MPEVMfCrdt/GDNpDbavYXiOXOdyJ5b3OPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.131.0': + resolution: {integrity: sha512-m+jNz9EuF0NXoiptc6B9h5yompZQVW/a5MJeOu5zojfH5yWk82tvF2ccrHkfhgtrS9h9DD5l1Qv8dWlfY7Nz8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.131.0': + resolution: {integrity: sha512-o14Hk8dAyiEUMFEWEgmAwFZvBt1RzAYLM3xeQ+5315JXgVYhoemivgYcbYVRbsFkS71ShMGlAFE0kPnr460rww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.131.0': + resolution: {integrity: sha512-PgnWDfV0h+b16XNKbXU7Daib/BFSt/J2mEzfYIBu6JB/wNdlU+kVYXCkGA1A9fWkTbOgbjh4e6NhPeQOYvFhEA==} + + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==, tarball: https://registry.npmmirror.com/@oxc-project/types/-/types-0.143.0.tgz} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, tarball: https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==, tarball: https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz} + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@tiptap/core@3.29.2': + resolution: {integrity: sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==} + peerDependencies: + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-blockquote@3.29.2': + resolution: {integrity: sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-bold@3.29.2': + resolution: {integrity: sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-bubble-menu@3.29.2': + resolution: {integrity: sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-bullet-list@3.29.2': + resolution: {integrity: sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-code-block@3.29.2': + resolution: {integrity: sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-code@3.29.2': + resolution: {integrity: sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-document@3.29.2': + resolution: {integrity: sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-dropcursor@3.29.2': + resolution: {integrity: sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==} + peerDependencies: + '@tiptap/extensions': 3.29.2 + + '@tiptap/extension-floating-menu@3.29.2': + resolution: {integrity: sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-font-family@3.29.2': + resolution: {integrity: sha512-HDq9+V8LbDFH1aQRH5Au0pmoDQ7bfa/C/k50a9mujzmA/PE7mLQkduUFfyDdiN4L60p7TVJAOJaN/loGC8PDxA==} + peerDependencies: + '@tiptap/extension-text-style': 3.29.2 + + '@tiptap/extension-gapcursor@3.29.2': + resolution: {integrity: sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==} + peerDependencies: + '@tiptap/extensions': 3.29.2 + + '@tiptap/extension-hard-break@3.29.2': + resolution: {integrity: sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-heading@3.29.2': + resolution: {integrity: sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-horizontal-rule@3.29.2': + resolution: {integrity: sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-italic@3.29.2': + resolution: {integrity: sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-link@3.29.2': + resolution: {integrity: sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-list-item@3.29.2': + resolution: {integrity: sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-list-keymap@3.29.2': + resolution: {integrity: sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-list@3.29.2': + resolution: {integrity: sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-ordered-list@3.29.2': + resolution: {integrity: sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-paragraph@3.29.2': + resolution: {integrity: sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-strike@3.29.2': + resolution: {integrity: sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-text-style@3.29.2': + resolution: {integrity: sha512-aQad9V9ROaEi3hE4g5z9wQ6lv5FSnzlnWFMVJxRjNlwXgm7H0fymxb4rGfca+pAwYkiRwpKYh1LatpJw2ECQAA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-text@3.29.2': + resolution: {integrity: sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-underline@3.29.2': + resolution: {integrity: sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extensions@3.29.2': + resolution: {integrity: sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/pm@3.29.2': + resolution: {integrity: sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==} + + '@tiptap/starter-kit@3.29.2': + resolution: {integrity: sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==} + + '@tiptap/vue-3@3.29.2': + resolution: {integrity: sha512-rLX+V+HXbsVlk+My8mslvbnbVXZiQZHYtbOiRgxugsqZ9SK1mr1FpgZ/ZNcotJAjecT8FJmPa45ccDnVu66eGA==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + vue: ^3.0.0 + + '@tsconfig/node24@24.0.4': + resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==, tarball: https://registry.npmmirror.com/@tsconfig/node24/-/node24-24.0.4.tgz} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==, tarball: https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==, tarball: https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==, tarball: https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz} + + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@unocss/cli@66.7.5': + resolution: {integrity: sha512-fgWkECRn2LGVo9sEpmjk/KJ3NSKUDitaZCNrJcEYM93DG+ELriTHcL+VWBlF4rcZf9fdGlq1nKbbRHUZirFCHQ==} + hasBin: true + + '@unocss/config@66.7.5': + resolution: {integrity: sha512-dkPl9glhEahJ+Xoja5ZseKKnH+vZaeaKQzg8b0otcKcPPNrHQgu1nu3QgfeOjnXOGrjZIotHwUeVtt4ZA2Skgg==} + + '@unocss/core@66.7.5': + resolution: {integrity: sha512-UdJb8MiMywcau8QrWEVgUAz0kvoFHyR+sACwYCgmBh/BpKJGyR/zw/Ys3wvysbm0f+i/20VGBex/QQxYVlzdyQ==} + + '@unocss/extractor-arbitrary-variants@66.7.5': + resolution: {integrity: sha512-5zOkbnLIJc8E749qNjLXfPHl3FdHloop12h706/ojr6idK4ix0KLm43R//uLnphixCehdHJRuHnavnM4C/kQbA==} + + '@unocss/inspector@66.7.5': + resolution: {integrity: sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==} + + '@unocss/preset-attributify@66.7.5': + resolution: {integrity: sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==} + + '@unocss/preset-icons@66.7.5': + resolution: {integrity: sha512-ZsxadWnGGtHdANTikuMnjkQaT1qwUFJHZBF4fzVsPMnUiiKGs8rzXukwM9w3Gi9ontM7nbG2FYi9clWETSlpFg==} + + '@unocss/preset-mini@66.7.5': + resolution: {integrity: sha512-o37TSl4ecT0dKu+3/TYuTYht75h82SEwDNl476m9ve0KW4Pv1O2tT/9TWd7N5cutEpySr8/YtjMwtWBD+drxBg==} + + '@unocss/preset-tagify@66.7.5': + resolution: {integrity: sha512-/8YB1tXVi+WmNKPhsCdtO1xnQKEkshSnWDp6AbvVP3f6qOF7adlMYpAt3kpWCoVUBsfOQ06ZQlnfricMjObyoQ==} + + '@unocss/preset-typography@66.7.5': + resolution: {integrity: sha512-2dxC8LT9KYa29UZT4muDFl7DbIXvKktTfeSMbUGMdZKbHcuMtKSP2U52wti1PL5I9duqp4v+8G33EeVutGTUaQ==} + + '@unocss/preset-uno@66.7.5': + resolution: {integrity: sha512-zaUlYgNngbt50fZA/LbtsEnmPKojPqeiXCyUUiKQMv2uJQhncR32xhLZXVQ+TJD7hlfZ+6FAqlco1NIiAcub9w==} + + '@unocss/preset-web-fonts@66.7.5': + resolution: {integrity: sha512-OLLTK7kswdSu51qxEm6O+AehecgCfS08Ivqo+280lKzFW7V1jXr5okeJ1ty+85oHCprJqzcD9iG1khxNRLomcA==} + + '@unocss/preset-wind3@66.7.5': + resolution: {integrity: sha512-atFe/7Qein+oMdyZs0hEo+3EjV99Av2LVxDbzpCungxIfbS3TnQt70EIuHXMPNCVWLYwrMV+/P1n9Ntb0E3mow==} + + '@unocss/preset-wind4@66.7.5': + resolution: {integrity: sha512-n3jIvQv8x1jXpmWfvNFBIqHNARki4mKZXfxAA31gekpXsRRTg16u1+yC1+PBBJgoEDFEnnmKnG7EZP88S8LVXA==} + + '@unocss/preset-wind@66.7.5': + resolution: {integrity: sha512-LVKgGr0A9Lc7F85xGXrrb71DDXMlHGA8bcj/Kht8BCsuRdBZadNbLlnv5qnSJiHYhi3/blzcgVoaeRor6L9yNA==} + + '@unocss/rule-utils@66.7.5': + resolution: {integrity: sha512-/AKHBRF6ZOexE3EDDv8ZEYh40P5e2Eha41IYUbE1wjigW7++ssmvimSTdufJzZvU5DGP++E3B3WEsFPprZMjAg==} + + '@unocss/transformer-attributify-jsx@66.7.5': + resolution: {integrity: sha512-r8qDwNSt0eGSwWws3xsuIHb3PZYR2embFdYoE67hD/xlYgLjX1uPy/HUlGCSSh4uLc4vVYjurr0Oq5xF/B8YiA==} + + '@unocss/transformer-compile-class@66.7.5': + resolution: {integrity: sha512-KuECgsGF7tGe808vIvKViEjI0UCUgYgneJfK+/TWBkjC7s8dLVaUtJzzcP9/r6OfGlgyn/x1YTdRqTaCENa7Xg==} + + '@unocss/transformer-directives@66.7.5': + resolution: {integrity: sha512-VMJApXXOwlDubkW+cNMmOkfxLefFupJr+yU23gGYUB2NPsuVltc8H5CDM0gfVebpeL5CUW05evxzEAk2wsju+Q==} + + '@unocss/transformer-variant-group@66.7.5': + resolution: {integrity: sha512-iDmP3mM8J+IxuQf5Uh33zsi528xnGxTpKRJ0c+LCrqBTTkR2tZr4aCzNmJ0g29Js2yEOsyNXRRc8BNTuvgn0AA==} + + '@unocss/vite@66.7.5': + resolution: {integrity: sha512-1z1TBCNJCR2WzjPtjzmCHr8rtzXHosMjLdsCNe6Mzsfwiw044gzzLVuiEZO9vIjkI50lvQ+gIOxOiVQG0hGAeA==} + peerDependencies: + vite: ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==, tarball: https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vitest/coverage-v8@3.2.7': + resolution: {integrity: sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==} + peerDependencies: + '@vitest/browser': 3.2.7 + vitest: 3.2.7 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==, tarball: https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==, tarball: https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==, tarball: https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==, tarball: https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==, tarball: https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==, tarball: https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==, tarball: https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==, tarball: https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==, tarball: https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==, tarball: https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz} + + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} + + '@vue/devtools-core@8.2.1': + resolution: {integrity: sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==, tarball: https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.2.1.tgz} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==, tarball: https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz} + + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==, tarball: https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz} + + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==, tarball: https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.9.tgz} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==, tarball: https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.41.tgz} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==, tarball: https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.41.tgz} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==, tarball: https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==, tarball: https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.41.tgz} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==, tarball: https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz} + + '@vue/tsconfig@0.9.1': + resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==, tarball: https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.9.1.tgz} + peerDependencies: + typescript: '>= 5.8' + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + '@vueuse/core@14.4.0': + resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.4.0': + resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} + + '@vueuse/shared@14.4.0': + resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} + peerDependencies: + vue: ^3.5.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==, tarball: https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz} + engines: {node: '>=0.4.0'} + hasBin: true + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==, tarball: https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansi-styles@7.0.0: + resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==, tarball: https://registry.npmmirror.com/ansi-styles/-/ansi-styles-7.0.0.tgz} + engines: {node: '>=22'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==, tarball: https://registry.npmmirror.com/ansis/-/ansis-4.3.1.tgz} + engines: {node: '>=14'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==, tarball: https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.12: + resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==, tarball: https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz} + engines: {node: '>=6.0.0'} + hasBin: true + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==, tarball: https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==, tarball: https://registry.npmmirror.com/birpc/-/birpc-4.0.0.tgz} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==, tarball: https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==, tarball: https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz} + engines: {node: '>=18'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==, tarball: https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz} + + canvas@3.2.3: + resolution: {integrity: sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==} + engines: {node: ^18.12.0 || >= 20.9.0} + + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==, tarball: https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz} + engines: {node: '>= 20.19.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==, tarball: https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz} + + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz} + engines: {node: '>= 8'} + + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==, tarball: https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==, tarball: https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, tarball: https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==, tarball: https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==, tarball: https://registry.npmmirror.com/date-fns/-/date-fns-4.4.0.tgz} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==, tarball: https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==, tarball: https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==, tarball: https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz} + engines: {node: '>=8'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.401: + resolution: {integrity: sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==, tarball: https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, tarball: https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, tarball: https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, tarball: https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==, tarball: https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fabric@7.4.0: + resolution: {integrity: sha512-NalYDc3eifTl1C33zryQwpH6+XA/2ClxQrH9vkASkZw3tbkRmorpikhYMmxhUTmi7O3e9ODz0vOT8qfaCh9IVA==} + engines: {node: '>=20.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + font-family-papandreou@0.2.0-patch2: + resolution: {integrity: sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + happy-dom@20.11.2: + resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} + engines: {node: '>=20.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==, tarball: https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz} + engines: {node: '>=12.0.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==, tarball: https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==, tarball: https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==, tarball: https://registry.npmmirror.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==, tarball: https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz} + engines: {node: '>=14.16'} + hasBin: true + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==, tarball: https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==, tarball: https://registry.npmmirror.com/isexe/-/isexe-4.0.0.tgz} + engines: {node: '>=20'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, tarball: https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@6.0.0: + resolution: {integrity: sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==, tarball: https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, tarball: https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz} + engines: {node: '>=6'} + hasBin: true + + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==, tarball: https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==, tarball: https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz} + engines: {node: '>= 12.0.0'} + + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==, tarball: https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==, tarball: https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, tarball: https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz} + + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==, tarball: https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz} + engines: {node: '>= 0.10.0'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==, tarball: https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, tarball: https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==, tarball: https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + naive-ui@2.44.1: + resolution: {integrity: sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==, tarball: https://registry.npmmirror.com/naive-ui/-/naive-ui-2.44.1.tgz} + engines: {node: '>=20'} + peerDependencies: + vue: ^3.0.0 + + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-3.3.17.tgz} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-releases@2.0.52: + resolution: {integrity: sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==, tarball: https://registry.npmmirror.com/node-releases/-/node-releases-2.0.52.tgz} + engines: {node: '>=18'} + + nostics@1.2.0: + resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==, tarball: https://registry.npmmirror.com/nostics/-/nostics-1.2.0.tgz} + + npm-normalize-package-bin@6.0.0: + resolution: {integrity: sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==, tarball: https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + npm-run-all2@9.0.3: + resolution: {integrity: sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==, tarball: https://registry.npmmirror.com/npm-run-all2/-/npm-run-all2-9.0.3.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0, npm: '>= 10'} + hasBin: true + + nwsapi@2.2.24: + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==, tarball: https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==, tarball: https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==, tarball: https://registry.npmmirror.com/open/-/open-11.0.0.tgz} + engines: {node: '>=20'} + + opentype.js@2.0.0: + resolution: {integrity: sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==} + hasBin: true + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + oxc-parser@0.131.0: + resolution: {integrity: sha512-SJ3/7ZPbgie8dr5Z9BI/M51zZbpXba+hRSG0MDzVwMW5CRQg2fjYE0jHGlLX4eeiibGgC/mzoDFKSDHwVZEHRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-walker@0.7.0: + resolution: {integrity: sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==} + peerDependencies: + oxc-parser: '>=0.98.0' + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==, tarball: https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==, tarball: https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==, tarball: https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz} + engines: {node: '>=12'} + + pidtree@1.0.0: + resolution: {integrity: sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==, tarball: https://registry.npmmirror.com/pidtree/-/pidtree-1.0.0.tgz} + engines: {node: '>=18'} + hasBin: true + + pinia@4.0.2: + resolution: {integrity: sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==, tarball: https://registry.npmmirror.com/pinia/-/pinia-4.0.2.tgz} + peerDependencies: + '@vue/devtools-api': ^8.1.5 + typescript: '>=5.6.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==, tarball: https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==, tarball: https://registry.npmmirror.com/postcss/-/postcss-8.5.25.tgz} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==, tarball: https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz} + engines: {node: '>=20'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==, tarball: https://registry.npmmirror.com/prettier/-/prettier-3.9.5.tgz} + engines: {node: '>=14'} + hasBin: true + + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.2: + resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==} + + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.42.2: + resolution: {integrity: sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + read-package-json-fast@6.0.0: + resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==, tarball: https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==, tarball: https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz} + engines: {node: '>= 20.19.0'} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==, tarball: https://registry.npmmirror.com/rolldown/-/rolldown-1.2.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==, tarball: https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz} + engines: {node: '>=18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + seemly@0.3.10: + resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==, tarball: https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz} + engines: {node: '>=8'} + + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==, tarball: https://registry.npmmirror.com/shell-quote/-/shell-quote-1.10.0.tgz} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, tarball: https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz} + engines: {node: '>=0.10.0'} + + specificity@0.4.1: + resolution: {integrity: sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==} + hasBin: true + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + + svg2pdf.js@2.7.0: + resolution: {integrity: sha512-nXK4Wx28H0KtOktanm5nsphl1KMEoLNMelAT/776qxPAj9DshwYcqgdpKuBnY1nrcYOriQFHVQLE4tIag+aDJA==} + peerDependencies: + jspdf: ^4.0.0 || ^3.0.0 || ^2.0.0 + + svgpath@2.6.0: + resolution: {integrity: sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, tarball: https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, tarball: https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==, tarball: https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==, tarball: https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==, tarball: https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==, tarball: https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz} + + unocss@66.7.5: + resolution: {integrity: sha512-nAdmU8TwnQoiLnQjZ6Hm1GmHy9lTexKsAZNEJtZnxN9wyFRc1eLjQgmh9r7UEGxBQMQLD8OZOgmk5HpwObbh0Q==} + peerDependencies: + '@unocss/astro': 66.7.5 + '@unocss/postcss': 66.7.5 + '@unocss/webpack': 66.7.5 + peerDependenciesMeta: + '@unocss/astro': + optional: true + '@unocss/postcss': + optional: true + '@unocss/webpack': + optional: true + + unplugin-utils@0.3.2: + resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==, tarball: https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.2.tgz} + engines: {node: '>=20.19.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, tarball: https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==, tarball: https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz} + peerDependencies: + vue: ^3.0.11 + + vite-dev-rpc@2.0.0: + resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==, tarball: https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-2.0.0.tgz} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0 + + vite-hot-client@2.2.0: + resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==, tarball: https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.2.0.tgz} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0 + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-inspect@11.4.1: + resolution: {integrity: sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==, tarball: https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.4.1.tgz} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.2.1: + resolution: {integrity: sha512-5JLxXWWCo5lJMw16/xVeNvJ8k2zLwZPf1vITLzya/2IePrCBeGe/p/iAokgXHZpEi39fcYtPXmO8SaKeXmqCAA==, tarball: https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.2.1.tgz} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite-plugin-vue-inspector@6.0.0: + resolution: {integrity: sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==, tarball: https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==, tarball: https://registry.npmmirror.com/vite/-/vite-8.2.0.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.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 + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==, tarball: https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz} + peerDependencies: + vue: ^3.0.0 + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==, tarball: https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz} + + vue-codemirror6@1.6.1: + resolution: {integrity: sha512-0Ue6nWy055UKP/6LkT0I7nW5SRSFJ+3mokwBdHePQsj11T02mrg3A0jtHyBqH23nRHQFxd3NKw63sZJ1ygo5dA==} + engines: {node: ^22.18.0 || >=24.12.0} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/commands': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/lint': ^6.0.0 + '@codemirror/search': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + codemirror: ^6.0.0 + style-mod: ^4.0.0 + vue: ^2.7.14 || ^3.3.4 + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==, tarball: https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.9.tgz} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==, tarball: https://registry.npmmirror.com/vue/-/vue-3.5.41.tgz} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vueuc@0.4.65: + resolution: {integrity: sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==, tarball: https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz} + peerDependencies: + vue: ^3.0.11 + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==, tarball: https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmmirror.com/which/-/which-2.0.2.tgz} + engines: {node: '>= 8'} + hasBin: true + + which@7.0.0: + resolution: {integrity: sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==, tarball: https://registry.npmmirror.com/which/-/which-7.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.2: + resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==, tarball: https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.3.1.tgz} + engines: {node: '>=20'} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, tarball: https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + optional: true + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.8 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bwip-js/generic@4.11.2': {} + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.8': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.41(typescript@6.0.3))': + dependencies: + vue: 3.5.41(typescript@6.0.3) + + '@csstools/color-helpers@5.1.0': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-tokenizer@3.0.4': + optional: true + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/hash@0.8.0': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@iconify-json/carbon@1.2.25': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@juggle/resize-observer@3.4.0': {} + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.3': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.131.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.131.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.131.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.131.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.131.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.131.0': + optional: true + + '@oxc-project/types@0.131.0': {} + + '@oxc-project/types@0.143.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-x64@1.2.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.3': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@tiptap/core@3.29.2(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-blockquote@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-bold@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-bubble-menu@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + optional: true + + '@tiptap/extension-bullet-list@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-code-block@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-code@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-document@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-dropcursor@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-floating-menu@3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + optional: true + + '@tiptap/extension-font-family@3.29.2(@tiptap/extension-text-style@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)))': + dependencies: + '@tiptap/extension-text-style': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + + '@tiptap/extension-gapcursor@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-hard-break@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-heading@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-horizontal-rule@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-italic@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-link@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-list-keymap@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-ordered-list@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-paragraph@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-strike@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-text-style@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-text@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-underline@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/pm@3.29.2': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.2 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + '@tiptap/starter-kit@3.29.2': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/extension-blockquote': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-bold': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-bullet-list': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-code': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-code-block': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-document': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-dropcursor': 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-gapcursor': 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-hard-break': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-heading': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-horizontal-rule': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-italic': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-link': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-list-item': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-list-keymap': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-ordered-list': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-paragraph': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-strike': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-text': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-underline': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/vue-3@3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-floating-menu': 3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tsconfig/node24@24.0.4': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/pako@2.0.4': {} + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.13.3 + + '@types/raf@3.4.3': + optional: true + + '@types/trusted-types@2.0.7': + optional: true + + '@types/web-bluetooth@0.0.21': {} + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.3 + + '@unocss/cli@66.7.5': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/preset-wind4': 66.7.5 + '@unocss/transformer-directives': 66.7.5 + cac: 7.0.0 + chokidar: 5.0.0 + colorette: 2.0.20 + consola: 3.4.2 + magic-string: 0.30.21 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyglobby: 0.2.17 + unplugin-utils: 0.3.2 + + '@unocss/config@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + colorette: 2.0.20 + consola: 3.4.2 + unconfig: 7.5.0 + + '@unocss/core@66.7.5': {} + + '@unocss/extractor-arbitrary-variants@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/inspector@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.2 + + '@unocss/preset-attributify@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/preset-icons@66.7.5': + dependencies: + '@iconify/utils': 3.1.4 + '@unocss/core': 66.7.5 + ofetch: 1.5.1 + + '@unocss/preset-mini@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/extractor-arbitrary-variants': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-tagify@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/preset-typography@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-uno@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + + '@unocss/preset-web-fonts@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + ofetch: 1.5.1 + + '@unocss/preset-wind3@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-mini': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-wind4@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/extractor-arbitrary-variants': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-wind@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + + '@unocss/rule-utils@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + magic-string: 0.30.21 + + '@unocss/transformer-attributify-jsx@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + oxc-parser: 0.131.0 + oxc-walker: 0.7.0(oxc-parser@0.131.0) + + '@unocss/transformer-compile-class@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/transformer-directives@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + css-tree: 3.2.1 + + '@unocss/transformer-variant-group@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/vite@66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/inspector': 66.7.5 + chokidar: 5.0.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.17 + unplugin-utils: 0.3.2 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + + '@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vue: 3.5.41(typescript@6.0.3) + + '@vitest/coverage-v8@3.2.7(vitest@3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.7) + '@vue/shared': 3.5.41 + optionalDependencies: + '@babel/core': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/parser': 7.29.8 + '@vue/compiler-sfc': 3.5.41 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.25 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/devtools-api@8.2.1': + dependencies: + '@vue/devtools-kit': 8.2.1 + + '@vue/devtools-core@8.2.1(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@vue/devtools-kit': 8.2.1 + '@vue/devtools-shared': 8.2.1 + vue: 3.5.41(typescript@6.0.3) + + '@vue/devtools-kit@8.2.1': + dependencies: + '@vue/devtools-shared': 8.2.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.2.1': {} + + '@vue/language-core@3.3.9': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.5 + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + + '@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))': + optionalDependencies: + typescript: 6.0.3 + vue: 3.5.41(typescript@6.0.3) + + '@vueuse/core@14.4.0(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.4.0 + '@vueuse/shared': 14.4.0(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + + '@vueuse/metadata@14.4.0': {} + + '@vueuse/shared@14.4.0(vue@3.5.41(typescript@6.0.3))': + dependencies: + vue: 3.5.41(typescript@6.0.3) + + acorn@8.18.0: {} + + adler-32@1.3.1: {} + + agent-base@7.1.4: + optional: true + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@3.2.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansi-styles@7.0.0: {} + + ansis@4.3.1: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + async-validator@4.2.5: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-arraybuffer@1.0.2: + optional: true + + base64-js@1.5.1: + optional: true + + baseline-browser-mapping@2.11.12: {} + + birpc@2.9.0: {} + + birpc@4.0.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.12 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.401 + node-releases: 2.0.52 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + buffer-image-size@0.6.4: + dependencies: + '@types/node': 24.13.3 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@6.7.14: {} + + cac@7.0.0: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001806: {} + + canvas@3.2.3: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + optional: true + + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.29.7 + '@types/raf': 3.4.3 + core-js: 3.50.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@1.1.4: + optional: true + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + + codepage@1.15.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + core-js@3.50.0: + optional: true + + crc-32@1.2.2: {} + + crelt@1.0.7: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + optional: true + + csstype@3.0.11: {} + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + optional: true + + date-fns-tz@3.2.0(date-fns@4.4.0): + dependencies: + date-fns: 4.4.0 + + date-fns@4.4.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js@10.6.0: + optional: true + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: + optional: true + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + dijkstrajs@1.0.3: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.401: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + + entities@6.0.1: + optional: true + + entities@7.0.1: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + evtd@0.2.4: {} + + expand-template@2.0.3: + optional: true + + expect-type@1.4.0: {} + + fabric@7.4.0: + optionalDependencies: + canvas: 3.2.3 + jsdom: 26.1.0(canvas@3.2.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + fast-deep-equal@3.1.3: {} + + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.2.0 + + fast-uri@3.1.5: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fflate@0.8.3: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + font-family-papandreou@0.2.0-patch2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + frac@1.1.2: {} + + fs-constants@1.0.0: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + github-from-package@0.0.0: + optional: true + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + happy-dom@20.11.2: + dependencies: + '@types/node': 24.13.3 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + has-flag@4.0.0: {} + + highlight.js@11.11.1: {} + + hookable@5.5.3: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + optional: true + + html-escaper@2.0.2: {} + + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: + optional: true + + import-meta-resolve@4.2.0: {} + + inherits@2.0.4: + optional: true + + ini@1.3.8: + optional: true + + iobuffer@5.4.0: {} + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-potential-custom-element-name@1.0.1: + optional: true + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@4.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsdom@26.1.0(canvas@3.2.3): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.2 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 3.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsesc@3.1.0: {} + + json-parse-even-better-errors@6.0.0: {} + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.29.7 + fast-png: 6.4.0 + fflate: 0.8.3 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.50.0 + dompurify: 3.4.13 + html2canvas: 1.4.1 + + kolorist@1.8.0: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + linkifyjs@4.3.3: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash-es@4.18.1: {} + + lodash@4.18.1: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.21 + mlly: 1.8.2 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.4 + unplugin: 2.3.11 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + mdn-data@2.27.1: {} + + memorystream@0.3.1: {} + + mimic-response@3.1.0: + optional: true + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: + optional: true + + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: + optional: true + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mustache@4.2.0: {} + + naive-ui@2.44.1(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.41(typescript@6.0.3)) + '@types/lodash': 4.17.25 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.2.3 + date-fns: 4.4.0 + date-fns-tz: 3.2.0(date-fns@4.4.0) + evtd: 0.2.4 + highlight.js: 11.11.1 + lodash: 4.18.1 + lodash-es: 4.18.1 + seemly: 0.3.10 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.41(typescript@6.0.3)) + vooks: 0.2.12(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + vueuc: 0.4.65(vue@3.5.41(typescript@6.0.3)) + + nanoid@3.3.17: {} + + napi-build-utils@2.0.0: + optional: true + + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + optional: true + + node-addon-api@7.1.1: + optional: true + + node-fetch-native@1.6.7: {} + + node-releases@2.0.52: {} + + nostics@1.2.0: {} + + npm-normalize-package-bin@6.0.0: {} + + npm-run-all2@9.0.3: + dependencies: + ansi-styles: 7.0.0 + cross-spawn: 7.0.6 + memorystream: 0.3.1 + picomatch: 4.0.5 + pidtree: 1.0.0 + read-package-json-fast: 6.0.0 + shell-quote: 1.10.0 + which: 7.0.0 + + nwsapi@2.2.24: + optional: true + + obug@2.1.4: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + opentype.js@2.0.0: {} + + orderedmap@2.1.1: {} + + oxc-parser@0.131.0: + dependencies: + '@oxc-project/types': 0.131.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.131.0 + '@oxc-parser/binding-android-arm64': 0.131.0 + '@oxc-parser/binding-darwin-arm64': 0.131.0 + '@oxc-parser/binding-darwin-x64': 0.131.0 + '@oxc-parser/binding-freebsd-x64': 0.131.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.131.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.131.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.131.0 + '@oxc-parser/binding-linux-arm64-musl': 0.131.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.131.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.131.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.131.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.131.0 + '@oxc-parser/binding-linux-x64-gnu': 0.131.0 + '@oxc-parser/binding-linux-x64-musl': 0.131.0 + '@oxc-parser/binding-openharmony-arm64': 0.131.0 + '@oxc-parser/binding-wasm32-wasi': 0.131.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.131.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.131.0 + '@oxc-parser/binding-win32-x64-msvc': 0.131.0 + + oxc-walker@0.7.0(oxc-parser@0.131.0): + dependencies: + magic-regexp: 0.10.0 + oxc-parser: 0.131.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.8.0: {} + + pako@2.2.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + optional: true + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@2.1.0: {} + + performance-now@2.1.0: + optional: true + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pidtree@1.0.0: {} + + pinia@4.0.2(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vue/devtools-api': 8.2.1 + nostics: 1.2.0 + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pngjs@5.0.0: {} + + postcss@8.5.25: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.94.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + optional: true + + prettier@3.9.5: {} + + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + punycode@2.3.1: + optional: true + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + quansync@1.0.0: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + read-package-json-fast@6.0.0: + dependencies: + json-parse-even-better-errors: 6.0.0 + npm-normalize-package-bin: 6.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + readdirp@5.0.0: {} + + regenerator-runtime@0.13.11: + optional: true + + regexp-tree@0.1.27: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + rgbcolor@1.0.1: + optional: true + + rolldown@1.2.3: + dependencies: + '@oxc-project/types': 0.143.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + rope-sequence@1.3.4: {} + + rrweb-cssom@0.8.0: + optional: true + + run-applescript@7.1.0: {} + + safe-buffer@5.2.1: + optional: true + + safer-buffer@2.1.2: + optional: true + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + + seemly@0.3.10: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-blocking@2.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.10.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + specificity@0.4.1: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + stackback@0.0.2: {} + + stackblur-canvas@2.7.0: + optional: true + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@2.0.1: + optional: true + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + style-mod@4.1.3: {} + + stylis@4.4.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svg-pathdata@6.0.3: + optional: true + + svg2pdf.js@2.7.0(jspdf@4.2.1): + dependencies: + cssesc: 3.0.0 + font-family-papandreou: 0.2.0-patch2 + jspdf: 4.2.1 + specificity: 0.4.1 + svgpath: 2.6.0 + + svgpath@2.6.0: {} + + symbol-tree@3.2.4: + optional: true + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.6 + + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + + totalist@3.0.1: {} + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + optional: true + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + optional: true + + treemate@0.3.11: {} + + ts-algebra@2.0.0: {} + + tslib@2.8.1: + optional: true + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + type-level-regexp@0.1.17: {} + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.7.0 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + undici-types@7.18.2: {} + + unocss@66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + '@unocss/cli': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-attributify': 66.7.5 + '@unocss/preset-icons': 66.7.5 + '@unocss/preset-mini': 66.7.5 + '@unocss/preset-tagify': 66.7.5 + '@unocss/preset-typography': 66.7.5 + '@unocss/preset-uno': 66.7.5 + '@unocss/preset-web-fonts': 66.7.5 + '@unocss/preset-wind': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/preset-wind4': 66.7.5 + '@unocss/transformer-attributify-jsx': 66.7.5 + '@unocss/transformer-compile-class': 66.7.5 + '@unocss/transformer-directives': 66.7.5 + '@unocss/transformer-variant-group': 66.7.5 + '@unocss/vite': 66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + transitivePeerDependencies: + - vite + + unplugin-utils@0.3.2: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.5 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.18.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: + optional: true + + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + + vdirs@0.1.8(vue@3.5.41(typescript@6.0.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.41(typescript@6.0.3) + + vite-dev-rpc@2.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + birpc: 4.0.0 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-hot-client: 2.2.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + + vite-hot-client@2.2.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-inspect@11.4.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + ansis: 4.3.1 + error-stack-parser-es: 1.0.5 + obug: 2.1.4 + ohash: 2.0.11 + open: 11.0.0 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.2 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-dev-rpc: 2.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + + vite-plugin-vue-devtools@8.2.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vue/devtools-core': 8.2.1(vue@3.5.41(typescript@6.0.3)) + '@vue/devtools-kit': 8.2.1 + '@vue/devtools-shared': 8.2.1 + sirv: 3.0.2 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-plugin-inspect: 11.4.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vite-plugin-vue-inspector: 6.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@6.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.7) + '@vue/compiler-dom': 3.5.41 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + + vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.25 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.33.0 + yaml: 2.9.0 + + vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.9.0 + + vitest@3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + happy-dom: 20.11.2 + jsdom: 26.1.0(canvas@3.2.3) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vooks@0.2.12(vue@3.5.41(typescript@6.0.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.41(typescript@6.0.3) + + vscode-uri@3.1.0: {} + + vue-codemirror6@1.6.1(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.8)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + codemirror: 6.0.2 + style-mod: 4.1.3 + vue: 3.5.41(typescript@6.0.3) + vue-demi: 0.14.10(vue@3.5.41(typescript@6.0.3)) + transitivePeerDependencies: + - '@vue/composition-api' + + vue-demi@0.14.10(vue@3.5.41(typescript@6.0.3)): + dependencies: + vue: 3.5.41(typescript@6.0.3) + + vue-tsc@3.3.9(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + typescript: 6.0.3 + + vue@3.5.41(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + optionalDependencies: + typescript: 6.0.3 + + vueuc@0.4.65(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.41(typescript@6.0.3)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.10 + vdirs: 0.1.8(vue@3.5.41(typescript@6.0.3)) + vooks: 0.2.12(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + + webidl-conversions@7.0.0: + optional: true + + webpack-virtual-modules@0.6.2: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + optional: true + + whatwg-mimetype@3.0.0: {} + + whatwg-mimetype@4.0.0: + optional: true + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + optional: true + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@7.0.0: + dependencies: + isexe: 4.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wmf@1.0.2: {} + + word@0.3.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: + optional: true + + ws@8.21.2: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xml-name-validator@5.0.0: + optional: true + + xmlchars@2.2.0: + optional: true + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yaml@2.9.0: + optional: true + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 diff --git a/openprint/pnpm-workspace.yaml b/openprint/pnpm-workspace.yaml new file mode 100644 index 0000000..1417f8a --- /dev/null +++ b/openprint/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - canvas + - core-js + - vue-demi diff --git a/openprint/public/favicon.ico b/openprint/public/favicon.ico new file mode 100644 index 0000000..41bdade Binary files /dev/null and b/openprint/public/favicon.ico differ diff --git a/openprint/public/fonts/ChillKai.woff2 b/openprint/public/fonts/ChillKai.woff2 new file mode 100644 index 0000000..40d6f8d Binary files /dev/null and b/openprint/public/fonts/ChillKai.woff2 differ diff --git a/openprint/public/fonts/HedvigLettersSans-Regular.woff2 b/openprint/public/fonts/HedvigLettersSans-Regular.woff2 new file mode 100644 index 0000000..9e589e6 Binary files /dev/null and b/openprint/public/fonts/HedvigLettersSans-Regular.woff2 differ diff --git a/openprint/public/fonts/LiberationSerif-Bold.woff2 b/openprint/public/fonts/LiberationSerif-Bold.woff2 new file mode 100644 index 0000000..067d104 Binary files /dev/null and b/openprint/public/fonts/LiberationSerif-Bold.woff2 differ diff --git a/openprint/public/fonts/LiberationSerif-Italic.woff2 b/openprint/public/fonts/LiberationSerif-Italic.woff2 new file mode 100644 index 0000000..4993810 Binary files /dev/null and b/openprint/public/fonts/LiberationSerif-Italic.woff2 differ diff --git a/openprint/public/fonts/LiberationSerif-Regular.woff2 b/openprint/public/fonts/LiberationSerif-Regular.woff2 new file mode 100644 index 0000000..0bdbc23 Binary files /dev/null and b/openprint/public/fonts/LiberationSerif-Regular.woff2 differ diff --git a/openprint/public/fonts/SourceHanSansCN-Normal.woff2 b/openprint/public/fonts/SourceHanSansCN-Normal.woff2 new file mode 100644 index 0000000..682b928 Binary files /dev/null and b/openprint/public/fonts/SourceHanSansCN-Normal.woff2 differ diff --git a/openprint/public/fonts/SourceHanSerifCN-Regular.ttf b/openprint/public/fonts/SourceHanSerifCN-Regular.ttf new file mode 100644 index 0000000..b5a828d Binary files /dev/null and b/openprint/public/fonts/SourceHanSerifCN-Regular.ttf differ diff --git a/openprint/screenshots/README.md b/openprint/screenshots/README.md new file mode 100644 index 0000000..4d6d6df --- /dev/null +++ b/openprint/screenshots/README.md @@ -0,0 +1,12 @@ +# 截图素材 + +将以下截图文件放入此目录,替换 README 中的配图: + +| 文件 | 说明 | 建议尺寸 | +|------|------|----------| +| `designer-overview.png` | 设计器界面全貌,展示控件库 + 画布 + 属性面板 | 1920×1080 | +| `ai-assistant.png` | AI 助手聊天窗口 + 生成的面单模板 | 1200×800 | +| `cloud-print.png` | 云打印架构拓扑图 | 1200×800 | +| `report-design.png` | 报表设计效果(表格 + 图表) | 1200×800 | +| `desktop-client.png` | C++ Qt 桌面客户端界面 | 1200×800 | +| `erp-integration.png` | ERP 对接流程示意图 | 1200×800 | \ No newline at end of file diff --git a/openprint/screenshots/ai-assistant.png b/openprint/screenshots/ai-assistant.png new file mode 100644 index 0000000..ef87363 Binary files /dev/null and b/openprint/screenshots/ai-assistant.png differ diff --git a/openprint/screenshots/cloud-print.jpg b/openprint/screenshots/cloud-print.jpg new file mode 100644 index 0000000..9b739ab Binary files /dev/null and b/openprint/screenshots/cloud-print.jpg differ diff --git a/openprint/screenshots/designer-overview.png b/openprint/screenshots/designer-overview.png new file mode 100644 index 0000000..e5f0d62 Binary files /dev/null and b/openprint/screenshots/designer-overview.png differ diff --git a/openprint/screenshots/desktop-client.jpg b/openprint/screenshots/desktop-client.jpg new file mode 100644 index 0000000..ad07a3a Binary files /dev/null and b/openprint/screenshots/desktop-client.jpg differ diff --git a/openprint/screenshots/erp-integration.jpg b/openprint/screenshots/erp-integration.jpg new file mode 100644 index 0000000..9704b23 Binary files /dev/null and b/openprint/screenshots/erp-integration.jpg differ diff --git a/openprint/screenshots/logo.png b/openprint/screenshots/logo.png new file mode 100644 index 0000000..243b966 Binary files /dev/null and b/openprint/screenshots/logo.png differ diff --git a/openprint/screenshots/report-design.png b/openprint/screenshots/report-design.png new file mode 100644 index 0000000..38ba268 Binary files /dev/null and b/openprint/screenshots/report-design.png differ diff --git a/openprint/src/App.vue b/openprint/src/App.vue new file mode 100644 index 0000000..be71e3a --- /dev/null +++ b/openprint/src/App.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/openprint/src/ai/ai.spec.ts b/openprint/src/ai/ai.spec.ts new file mode 100644 index 0000000..b8d9a38 --- /dev/null +++ b/openprint/src/ai/ai.spec.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest' +import { extractJson } from './generate' +import { normalizeTemplate } from './normalize' +import { validateTemplate } from '@/core/spec/validator' + +describe('AI 核心:JSON 提取', () => { + it('从 ```json 代码块提取', () => { + const text = '说明一下:\n```json\n{"version":"1.0.0","document":{"type":"report"}}\n```' + const r = extractJson(text) as Record + expect(r.version).toBe('1.0.0') + }) + + it('从纯 JSON 提取', () => { + const r = extractJson('{"a":1}') as Record + expect(r.a).toBe(1) + }) + + it('从夹带散文的文本中提取首尾花括号', () => { + const text = '好的,这是模板:{"document":{"type":"report","sections":[{"type":"body","components":[]}]}} 请查收' + const r = extractJson(text) as Record + expect((r.document as Record).type).toBe('report') + }) + + it('无 JSON 返回 null', () => { + expect(extractJson('没有模板')).toBeNull() + }) +}) + +describe('AI 核心:归一化 + 协议校验', () => { + it('补齐缺省字段并产出可通过校验的模板', () => { + const raw = { + document: { + page: { width: 100, height: 150, orientation: 'portrait' }, + sections: [ + { + type: 'body', + components: [ + { type: 'text', left: 0, top: 0, width: 84, height: 12, value: '标题' }, + ], + }, + ], + }, + } + const tpl = normalizeTemplate(raw) + expect(tpl.version).toBe('1.0.0') + expect(tpl.document.type).toBe('report') + // 缺省 unit 应为 mm,margin 应有默认值 + expect(tpl.document.page.unit).toBe('mm') + expect(tpl.document.page.margin.top).toBeGreaterThanOrEqual(0) + // 控件被自动补 id + const body = tpl.document.sections.find((s) => s.type === 'body')! + expect(body.components![0]!.id).toBeTruthy() + + const result = validateTemplate(tpl) + expect(result.valid).toBe(true) + }) + + it('横向页(width>height)推导 orientation', () => { + const tpl = normalizeTemplate({ + document: { + page: { width: 297, height: 210 }, + sections: [{ type: 'body', components: [] }], + }, + }) + expect(tpl.document.page.orientation).toBe('landscape') + }) + + it('坐标纠偏:page-origin 输出被还原为 content-relative', () => { + const raw = { + document: { + page: { width: 100, height: 150, margin: { top: 8, bottom: 8, left: 8, right: 8 } }, + sections: [ + { + type: 'body', + components: [ + { type: 'text', left: 8, top: 8, width: 84, height: 12, value: '标题' }, + { type: 'text', left: 8, top: 30, width: 84, height: 8, value: '正文' }, + ], + }, + ], + }, + } + const tpl = normalizeTemplate(raw) + const body = tpl.document.sections.find((s) => s.type === 'body')! + // 双轴最小坐标命中 margin(8) → 统一减去页边距,消除整页右移 + expect(body.components![0]!.left).toBe(0) + expect(body.components![0]!.top).toBe(0) + expect(body.components![1]!.top).toBeCloseTo(22, 6) + const result = validateTemplate(tpl) + expect(result.valid).toBe(true) + }) + + it('坐标纠偏:正确 content-relative 模板不应被误伤', () => { + const raw = { + document: { + page: { width: 100, height: 150, margin: { top: 8, bottom: 8, left: 8, right: 8 } }, + sections: [ + { + type: 'body', + components: [ + { type: 'text', left: 0, top: 0, width: 84, height: 12, value: '标题' }, + { type: 'text', left: 10, top: 20, width: 74, height: 8, value: '正文' }, + ], + }, + ], + }, + } + const tpl = normalizeTemplate(raw) + const body = tpl.document.sections.find((s) => s.type === 'body')! + expect(body.components![0]!.left).toBe(0) + expect(body.components![1]!.left).toBe(10) // 保持原值,未被偏移 + }) +}) diff --git a/openprint/src/ai/client.ts b/openprint/src/ai/client.ts new file mode 100644 index 0000000..ad5ddd5 --- /dev/null +++ b/openprint/src/ai/client.ts @@ -0,0 +1,150 @@ +/** + * AI 客户端 —— 纯前端直连 OpenAI 兼容 /chat/completions(SSE 流式)。 + * 不依赖任何后端;baseURL / apiKey / model 全部由用户在前端设置里提供。 + */ + +export interface ChatMessage { + role: 'system' | 'user' | 'assistant' + content: string +} + +export interface StreamOptions { + baseURL: string + apiKey: string + model: string + messages: ChatMessage[] + onToken: (delta: string) => void + signal?: AbortSignal +} + +/** LLM 调用错误(携带 HTTP 状态码,便于前端给出友好提示) */ +export class AiRequestError extends Error { + status: number + constructor(status: number, message: string) { + super(message) + this.name = 'AiRequestError' + this.status = status + } +} + +function friendlyError(status: number, body: string): string { + if (status === 401) return 'API Key 无效或已过期,请检查设置中的 Key。' + if (status === 404) return '接口地址不正确(请确认 baseURL 含 /v1 且路径为 /chat/completions)。' + if (status === 429) return '请求过于频繁或额度不足(429)。' + if (status >= 500) return `模型服务异常(HTTP ${status})。` + // 跨域 / 网络层:fetch 抛 TypeError,没有 status + const lower = body.toLowerCase() + if (lower.includes('cors') || lower.includes('cross-origin')) { + return '跨域(CORS)被拦截:浏览器直连该地址受限。可将 baseURL 改为你自己的代理(如 Cloudflare Worker / Vercel Edge),或在支持浏览器直连的模型服务上使用。' + } + return body ? `请求失败(HTTP ${status}):${body.slice(0, 200)}` : `请求失败(HTTP ${status})` +} + +/** + * 流式对话(SSE)。逐 token 回调 onToken,最终返回完整文本。 + * 兼容 OpenAI / DeepSeek / 通义 等 OpenAI 格式(data: {choices:[{delta:{content}}]})。 + */ +export async function streamChat(opts: StreamOptions): Promise { + const url = `${opts.baseURL.replace(/\/+$/, '')}/chat/completions` + + let res: Response + try { + res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${opts.apiKey}`, + }, + body: JSON.stringify({ + model: opts.model, + messages: opts.messages, + stream: true, + temperature: 0.6, + }), + signal: opts.signal, + }) + } catch (e) { + // 网络层错误(含 CORS):fetch 抛 TypeError,无 status + const reason = e instanceof Error ? e.message : String(e) + if (reason.toLowerCase().includes('cors') || reason.toLowerCase().includes('cross-origin')) { + throw new AiRequestError( + 0, + '跨域(CORS)被拦截:浏览器直连该地址受限。可将 baseURL 改为你自己的代理(如 Cloudflare Worker / Vercel Edge),或在支持浏览器直连的模型服务上使用。', + ) + } + if (e instanceof DOMException && e.name === 'AbortError') throw e + throw new AiRequestError(0, `无法连接模型服务:${reason}`) + } + + if (!res.ok) { + const txt = await res.text().catch(() => '') + throw new AiRequestError(res.status, friendlyError(res.status, txt)) + } + + if (!res.body) { + throw new AiRequestError(0, '响应缺少流式数据体。') + } + + const reader = res.body.getReader() + const decoder = new TextDecoder() + let full = '' + let buffer = '' + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + + let nl: number + while ((nl = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, nl).trim() + buffer = buffer.slice(nl + 1) + if (!line || line.startsWith(':')) continue + if (line === 'data: [DONE]') continue + if (!line.startsWith('data:')) continue + const payload = line.slice(5).trim() + if (!payload) continue + try { + const obj = JSON.parse(payload) + const delta: string | undefined = obj.choices?.[0]?.delta?.content + if (delta) { + full += delta + opts.onToken(delta) + } + } catch { + // 忽略不完整的分片 + } + } + } + } finally { + // 确保 reader 被释放(中断时也清理) + try { + await reader.cancel() + } catch { + /* noop */ + } + } + + // 兜底:部分 OpenAI 兼容端点忽略 stream:true,直接返回单次 JSON(choices[0].message.content)。 + // 这种情况下上面没有触发 onToken,这里做一次补偿,保证仍能解析出模板文本。 + if (!full) { + const trimmed = buffer.trim() + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + const obj = JSON.parse(trimmed) as { + choices?: Array<{ message?: { content?: string }; text?: string }> + } + const content = obj.choices?.[0]?.message?.content ?? obj.choices?.[0]?.text + if (content) { + full = content + opts.onToken(content) + } + } catch { + /* 不是纯 JSON,忽略 */ + } + } + } + + return full +} diff --git a/openprint/src/ai/generate.ts b/openprint/src/ai/generate.ts new file mode 100644 index 0000000..ceca61e --- /dev/null +++ b/openprint/src/ai/generate.ts @@ -0,0 +1,165 @@ +/** + * AI 生成编排 —— 组装提示词 → 流式调用 → 解析 JSON → 归一化 → 校验/修复。 + * 纯前端、一次性问答;不落库,结果由调用方决定如何应用。 + */ +import type { AnyControl } from '@/types/control' +import type { TemplateData } from '@/types/template' +import { validateTemplate } from '@/core/spec/validator' +import { streamChat, AiRequestError } from './client' +import { buildSystemPrompt, buildUserPrompt, getFewShot } from './schema' +import { normalizeControl, normalizeTemplate } from './normalize' +import type { AiSettings } from '@/config/ai-settings' + +export interface GenerateRequest { + prompt: string + /** 基于当前模板修改时传入 */ + currentTemplate?: TemplateData + /** 仅针对画布中选中的控件改写(C 功能):传入这些控件的协议 JSON */ + selectedControls?: AnyControl[] + /** 数据字段接地(可选) */ + datasourceFields?: string[] + settings: AiSettings + onToken?: (delta: string) => void + signal?: AbortSignal +} + +export interface GenerateResult { + ok: boolean + data?: TemplateData + /** 选区改写模式:返回的新控件集合(用于替换原选中控件) */ + controls?: AnyControl[] + error?: string + /** 模型原始输出(用于调试 / 展示) */ + raw?: string +} + +/** 从模型文本中提取 JSON(兼容 ```json 代码块 / 纯 JSON / 首尾花括号) */ +export function extractJson(text: string): unknown | null { + const trimmed = text.trim() + if (!trimmed) return null + + // 1) 直接可解析 + try { + return JSON.parse(trimmed) + } catch { + /* fallthrough */ + } + + // 2) ```json ... ``` 代码块 + const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i) + if (fence?.[1]) { + try { + return JSON.parse(fence[1].trim()) + } catch { + /* fallthrough */ + } + } + + // 3) 第一个 { 到最后一个 } + const first = trimmed.indexOf('{') + const last = trimmed.lastIndexOf('}') + if (first >= 0 && last > first) { + try { + return JSON.parse(trimmed.slice(first, last + 1)) + } catch { + /* fallthrough */ + } + } + return null +} + +function fmtError(e: unknown): string { + if (e instanceof AiRequestError) return e.message + if (e instanceof DOMException && e.name === 'AbortError') return '已取消生成。' + return e instanceof Error ? e.message : '未知错误' +} + +/** + * 生成模板。 + * - 普通 / 基于当前模板改:返回完整 TemplateData,失败时最多把校验错误回传模型重试一次。 + * - 选区改写(selectedControls):模型返回控件数组,归一化后返回 controls。 + */ +export async function generateTemplate(req: GenerateRequest): Promise { + const messages = [ + { role: 'system' as const, content: buildSystemPrompt() }, + ...getFewShot(), + { role: 'user' as const, content: buildUserPrompt(req) }, + ] + + // —— 选区改写模式:期望模型返回控件数组 —— + if (req.selectedControls && req.selectedControls.length) { + let raw = '' + try { + raw = await streamChat({ + baseURL: req.settings.baseURL, + apiKey: req.settings.apiKey, + model: req.settings.model, + messages, + onToken: (d) => req.onToken?.(d), + signal: req.signal, + }) + } catch (e) { + return { ok: false, error: fmtError(e), raw } + } + const json = extractJson(raw) + const arr = Array.isArray(json) ? json : [] + const controls = arr + .map((c) => (typeof c === 'object' && c ? normalizeControl(c as Record) : null)) + .filter((c): c is AnyControl => c !== null) + if (!controls.length) { + return { ok: false, error: '模型未返回有效的控件 JSON(应为控件数组)。', raw } + } + return { ok: true, controls, raw } + } + + // —— 完整模板模式 —— + let raw = '' + const MAX_ATTEMPTS = 2 + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + try { + raw = await streamChat({ + baseURL: req.settings.baseURL, + apiKey: req.settings.apiKey, + model: req.settings.model, + messages, + onToken: (d) => req.onToken?.(d), + signal: req.signal, + }) + } catch (e) { + return { ok: false, error: fmtError(e), raw } + } + + const json = extractJson(raw) + if (!json) { + return { ok: false, error: '模型未返回有效的模板 JSON。', raw } + } + + const normalized = normalizeTemplate(json) + const result = validateTemplate(normalized) + if (result.valid) { + return { ok: true, data: normalized, raw } + } + + // 重试:把校验错误反馈给模型 + if (attempt < MAX_ATTEMPTS - 1) { + const issues = result.issues + .map((i) => `${i.path || '/'}:${i.message}`) + .join(';') + messages.push({ role: 'assistant', content: raw }) + messages.push({ + role: 'user', + content: `你的输出未通过模板协议校验,请修正后只输出正确 JSON。错误:${issues}`, + }) + raw = '' + continue + } + return { + ok: false, + error: `模板校验失败:${result.issues + .map((i) => `${i.path || '/'}: ${i.message}`) + .join(';')}`, + raw, + } + } + return { ok: false, error: '生成失败(超出重试次数)。', raw } +} diff --git a/openprint/src/ai/normalize.ts b/openprint/src/ai/normalize.ts new file mode 100644 index 0000000..60772ee --- /dev/null +++ b/openprint/src/ai/normalize.ts @@ -0,0 +1,133 @@ +/** + * AI 输出归一化 —— 把模型返回的「接近协议」的 JSON 修成可通过校验的 TemplateData。 + * 设计器 loadTemplate 也会兜底缺 id,这里提前补齐让校验更稳。 + */ +import type { AnyControl, ControlType } from '@/types/control' +import type { TemplateData } from '@/types/template' + +const VALID_TYPES: ControlType[] = [ + 'text', + 'image', + 'table', + 'barcode', + 'qrcode', + 'richtext', + 'rect', + 'line', + 'zone', +] + +function genId(prefix = 'ctrl'): string { + return `${prefix}_${Math.random().toString(36).slice(2, 10)}` +} + +function num(v: unknown, fallback: number): number { + return typeof v === 'number' && Number.isFinite(v) ? v : fallback +} + +function round1(v: number): number { + return Math.round(v * 10) / 10 +} + +export function normalizeControl(raw: Record): AnyControl | null { + const type = raw.type as ControlType + if (!VALID_TYPES.includes(type)) return null + const base = { + id: typeof raw.id === 'string' && raw.id ? raw.id : genId(type), + type, + left: round1(num(raw.left, 0)), + top: round1(num(raw.top, 0)), + width: round1(Math.max(num(raw.width, 10), 1)), + height: round1(Math.max(num(raw.height, 6), 1)), + } + const extra: Record = { ...raw } + delete extra.id + delete extra.type + delete extra.left + delete extra.top + delete extra.width + delete extra.height + return { ...base, ...extra } as AnyControl +} + +function normalizeSection(raw: Record): Record | null { + const type = raw.type + if (type !== 'header' && type !== 'body' && type !== 'footer') return null + const components = Array.isArray(raw.components) + ? (raw.components as Record[]) + .map(normalizeControl) + .filter((c): c is AnyControl => c !== null) + : [] + return { + type, + ...(type !== 'body' ? { height: num(raw.height, 20) } : {}), + repeat: raw.repeat === false ? false : true, + components, + } +} + +/** 归一化整份模板 */ +export function normalizeTemplate(raw: unknown): TemplateData { + const obj = (raw && typeof raw === 'object' ? raw : {}) as Record + const doc = (obj.document && typeof obj.document === 'object' + ? obj.document + : {}) as Record + const page = (doc.page && typeof doc.page === 'object' ? doc.page : {}) as Record + const sectionsRaw = Array.isArray(doc.sections) ? (doc.sections as Record[]) : [] + + const sections = sectionsRaw + .map(normalizeSection) + .filter((s): s is Record => s !== null) + + // 至少保证有一个 body + if (!sections.some((s) => s.type === 'body')) { + sections.push({ type: 'body', components: [] }) + } + + // —— 坐标纠偏:把「相对页面(page-origin)」的 AI 输出还原为「相对内容区(content-relative)」 —— + // 触发条件:某节内控件的最小 left 接近 margin.left 且最小 top 接近 margin.top(即模型把页边距也算进了坐标)。 + // 此时统一减去页边距,避免整页内容向右下偏移一个 margin。正确生成的模板 minLeft/minTop≈0,不会触发。 + const ml = num((page.margin as Record)?.left, 10) + const mt = num((page.margin as Record)?.top, 10) + const EPS = 2 + for (const section of sections) { + const comps = (section.components as AnyControl[] | undefined) ?? [] + if (!comps.length) continue + const minLeft = Math.min(...comps.map((c) => c.left)) + const minTop = Math.min(...comps.map((c) => c.top)) + if (minLeft > 1 && Math.abs(minLeft - ml) <= EPS && minTop > 1 && Math.abs(minTop - mt) <= EPS) { + for (const c of comps) { + c.left = round1(c.left - ml) + c.top = round1(c.top - mt) + } + } + } + + const width = num(page.width, 210) + const height = num(page.height, 297) + + const normalized: TemplateData = { + version: typeof obj.version === 'string' ? obj.version : '1.0.0', + document: { + type: 'report', + page: { + width, + height, + unit: page.unit === 'in' || page.unit === 'pt' ? page.unit : 'mm', + orientation: + page.orientation === 'landscape' || width > height ? 'landscape' : 'portrait', + margin: { + top: num((page.margin as Record)?.top, 10), + bottom: num((page.margin as Record)?.bottom, 10), + left: num((page.margin as Record)?.left, 10), + right: num((page.margin as Record)?.right, 10), + }, + ...(typeof page.backgroundColor === 'string' + ? { backgroundColor: page.backgroundColor } + : {}), + }, + sections: sections as unknown as TemplateData['document']['sections'], + }, + } + return normalized +} diff --git a/openprint/src/ai/schema.ts b/openprint/src/ai/schema.ts new file mode 100644 index 0000000..4b0269f --- /dev/null +++ b/openprint/src/ai/schema.ts @@ -0,0 +1,155 @@ +/** + * AI 助手 —— 前端预设的提示词与 few-shot 示例(无后端、纯常量)。 + * + * 协议要点(节选自项目《OpenPrint-设计方案》§5): + * TemplateData = { version, document } + * document = { type:'report', page, sections:[...] } + * page = { width, height, unit:'mm', orientation:'portrait'|'landscape', + * margin:{top,bottom,left,right}, backgroundColor?, watermark? } + * section = { type:'header'|'body'|'footer', height?, repeat?, components:[...] } + * component 公共:{ id?, type, left, top, width, height, angle?, printable?, ... } + * 坐标系:mm。⚠️ left/top 是相对「内容区(页边距内侧)左上角」的偏移,渲染器会自动叠加页边距——你不要再算 margin 进去。 + */ + +const PROTOCOL_SUMMARY = `你是 OpenPrint 打印模板设计助手。根据用户用中文描述的排版需求,生成一份「打印模板」JSON。 + +模板协议(必须遵守): +- 顶层:{ "version": "1.0.0", "document": { "type": "report", "page": {...}, "sections": [...] } } +- page:{ "width": 数字, "height": 数字, "unit": "mm", "orientation": "portrait"|"landscape", "margin": { "top": 数字, "bottom": 数字, "left": 数字, "right": 数字 }, "backgroundColor"?: "#ffffff" }。横向时 width > height。常用纸张:A4 纵向 210×297、A4 横向 297×210、A5 纵向 148×210。 +- sections:数组。必须且只能有一个 type:"body";可选 type:"header" / "footer"(页眉页脚,带 height)。 +- 控件通用字段:type 必填;left/top/width/height 必填(单位 mm,原点 = 内容区左上角,即已扣除页边距);id 可省略(系统自动补)。 +- 控件类型与要点: + · text:静态文字用 value;如需绑数据用 binding(字段路径)或 expression(如 "{{order.total}}")。style 可选:{ fontSize(pt), fill(hex), fontWeight:'bold', textAlign:'left'|'center'|'right', lineHeight }。 + · image:{ value:{ mode:'url'|'binding', content } },fit 可选。 + · table:columns:[{ title, field?, width, align? }],options 可选(borders/zebra 等),dataSource 留空表示布局网格。 + · barcode:{ value, format:'CODE128'|'EAN13'..., showText? };qrcode:{ value, errorLevel? }。 + · rect / line:{ fill?, stroke?, strokeWidth?, cornerRadius? }。 + · richtext:{ value: "

HTML

" }(仅可信静态内容)。 + +设计原则: +- 间距与对齐要规整(建议控件左/右贴边留白一致,关键元素水平居中)。 +- 坐标铁律:left/top 是相对「内容区(页边距内侧)左上角」的 mm,渲染器会再叠加页边距,所以你写的坐标【不要包含 margin】。给定页面 width/height 与 margin 后,内容区宽 = width - margin.left - margin.right、高 = height - margin.top - margin.bottom;占满整行写 left:0, width:内容区宽,水平居中配合 style.textAlign:'center'。严禁出现 left 等于 margin.left、top 等于 margin.top 这类把页边距算进坐标的写法。 +- 只输出「一段简要中文说明(不超过 3 句)」+ 一个 \`\`\`json 代码块包裹的完整模板。不要输出其他内容。 +- 不依赖真实数据:文字用静态 value;除非用户明确给出字段,否则不要写 binding。 +- 颜色用 hex(如 #1677ff、#333333)或 rgba。` + +const FEW_SHOT: Array<{ role: 'user' | 'assistant'; content: string }> = [ + { + role: 'user', + content: '做一个竖向快递面单,100×150mm,上方标题"快递面单",中间收件人姓名电话和地址,底部一个条码和右下角一个二维码。', + }, + { + role: 'assistant', + content: `好的,已生成 100×150mm 的竖向快递面单:标题居中、收件信息分区、底部条码 + 右下角二维码。 +(坐标均为相对内容区:页面 100×150、四周 margin=8,内容区宽 = 100-8-8 = 84) + +\`\`\`json +{ + "version": "1.0.0", + "document": { + "type": "report", + "page": { + "width": 100, "height": 150, "unit": "mm", "orientation": "portrait", + "margin": { "top": 8, "bottom": 8, "left": 8, "right": 8 }, + "backgroundColor": "#ffffff" + }, + "sections": [ + { + "type": "body", + "components": [ + { "type": "text", "left": 0, "top": 2, "width": 84, "height": 12, "value": "快递面单", "style": { "fontSize": 20, "fontWeight": "bold", "textAlign": "center" } }, + { "type": "text", "left": 0, "top": 18, "width": 84, "height": 8, "value": "收件人:张三 13800000000", "style": { "fontSize": 11 } }, + { "type": "text", "left": 0, "top": 30, "width": 84, "height": 30, "value": "地址:广东省深圳市南山区科技园路 1 号 XX 大厦 10 层", "style": { "fontSize": 11 } }, + { "type": "text", "left": 0, "top": 64, "width": 84, "height": 8, "value": "托寄物:文件 到付", "style": { "fontSize": 11, "fill": "#555555" } }, + { "type": "barcode", "left": 0, "top": 78, "width": 84, "height": 22, "value": "SF1234567890", "format": "CODE128", "showText": true }, + { "type": "qrcode", "left": 52, "top": 104, "width": 32, "height": 28, "value": "https://example.com/order/SF1234567890" }, + { "type": "text", "left": 0, "top": 104, "width": 48, "height": 28, "value": "请于签收前核对信息", "style": { "fontSize": 10, "fill": "#888888" } } + ] + } + ] + } +} +\`\`\``, + }, + { + role: 'user', + content: '做一个横向会员卡,90×54mm,左侧品牌名和会员等级,右侧会员姓名和大号卡号,底部一条分割线。', + }, + { + role: 'assistant', + content: `已生成 90×54mm 横向会员卡:左侧品牌与等级、右侧姓名与卡号、底部分割线。 +(坐标相对内容区:页面 90×54、四周 margin=6,内容区宽 = 90-6-6 = 78) + +\`\`\`json +{ + "version": "1.0.0", + "document": { + "type": "report", + "page": { + "width": 90, "height": 54, "unit": "mm", "orientation": "landscape", + "margin": { "top": 6, "bottom": 6, "left": 6, "right": 6 }, + "backgroundColor": "#0c447c" + }, + "sections": [ + { + "type": "body", + "components": [ + { "type": "text", "left": 0, "top": 4, "width": 40, "height": 10, "value": "STAR 会员", "style": { "fontSize": 16, "fontWeight": "bold", "fill": "#ffffff" } }, + { "type": "text", "left": 0, "top": 18, "width": 40, "height": 8, "value": "钻石会员", "style": { "fontSize": 11, "fill": "#9fc3ff" } }, + { "type": "text", "left": 46, "top": 6, "width": 32, "height": 10, "value": "李雷", "style": { "fontSize": 15, "fontWeight": "bold", "fill": "#ffffff", "textAlign": "right" } }, + { "type": "text", "left": 46, "top": 20, "width": 32, "height": 10, "value": "NO. 8821 0043", "style": { "fontSize": 14, "fill": "#ffffff", "textAlign": "right", "letterSpacing": 1 } }, + { "type": "line", "left": 0, "top": 34, "width": 78, "height": 0.4, "stroke": "#ffffff", "strokeWidth": 0.4 } + ] + } + ] + } +} +\`\`\``, + }, +] + +/** 组装 system 提示词 */ +export function buildSystemPrompt(): string { + return PROTOCOL_SUMMARY +} + +/** 组装用户提示词(支持「基于当前模板修改」「仅选中控件改写」「数据字段接地」) */ +export function buildUserPrompt(opts: { + prompt: string + currentTemplate?: unknown + selectedControls?: unknown[] + datasourceFields?: string[] +}): string { + const parts: string[] = [] + if (opts.datasourceFields && opts.datasourceFields.length) { + parts.push(`可用数据字段(如用户要求绑定真实数据,请优先使用这些路径):\n${opts.datasourceFields.join('、')}\n`) + } + if (opts.selectedControls && opts.selectedControls.length) { + parts.push( + `下面是一组「用户当前选中的控件」(坐标 left/top 是相对内容区左上角的 mm)。\n` + + `请按需求改写这些选中控件并【只输出一个控件 JSON 数组】,要求:\n` + + `· 必须保留每个原控件的 id(用于原位替换),不要改变其 type;\n` + + `· 可调整 left/top/width/height/value/style 等,实现重排、对齐、换风格、统一间距等;\n` + + `· 可新增控件(给新的唯一 id,type 合法),也可删除某些选中控件(不输出即可);\n` + + `· 坐标仍是相对内容区(已扣除页边距),不要再加 margin;\n` + + `· 只输出控件数组,不要包装成完整模板。\n` + + '```json\n' + + JSON.stringify(opts.selectedControls, null, 2) + + '\n```', + ) + } else if (opts.currentTemplate) { + parts.push( + `请在下面这份「当前模板 JSON」的基础上按需求修改,输出完整的新模板(不要只输出 diff):\n` + + '```json\n' + + JSON.stringify(opts.currentTemplate, null, 2) + + '\n```', + ) + } + parts.push(`需求:${opts.prompt}`) + return parts.join('\n\n') +} + +/** few-shot 示例(user/assistant 成对) */ +export function getFewShot(): Array<{ role: 'user' | 'assistant'; content: string }> { + return FEW_SHOT +} diff --git a/openprint/src/assets/logo.png b/openprint/src/assets/logo.png new file mode 100644 index 0000000..9bf2017 Binary files /dev/null and b/openprint/src/assets/logo.png differ diff --git a/openprint/src/assets/main.css b/openprint/src/assets/main.css new file mode 100644 index 0000000..9c91254 --- /dev/null +++ b/openprint/src/assets/main.css @@ -0,0 +1,59 @@ +:root { + /* 旧变量保留(对齐文档,但品牌变量统一在 theme/brand.css 中) */ + --brand-primary: #1677ff; + --brand-dark: #111827; + --brand-bg: #f5f7fa; + --brand-border: #e5e7eb; +} + +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +/* naive-ui Provider 会在 #app 下包一层无样式 div,补齐高度链 */ +#app > div { + height: 100%; +} + +body { + font-family: + Inter, + 'Source Han Sans CN', + 'PingFang SC', + 'Microsoft YaHei', + system-ui, + -apple-system, + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: var(--brand-bg); + color: var(--brand-text-1); + overflow: hidden; +} + +/* html.dark 下 body 由 NGlobalStyle 同步 bodyColor;这里加一个过渡让切换更顺 */ +* { + transition: + background-color 0.2s ease, + color 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} +/* 画布/标尺/控件本身不需要过渡,否则拖拽有拖影 */ +canvas, +svg, +svg *, +.fabric-canvas, +canvas * { + transition: none !important; +} + +/* NInputNumber 内部 input 默认左对齐;button-placement="both" 时左右按钮挤压空间, + 统一居中更美观。覆盖内部 .n-input__input-el(naive-ui 无 textAlign prop)。 */ +.n-input-number .n-input__input-el { + text-align: center; +} diff --git a/openprint/src/config/ai-settings.ts b/openprint/src/config/ai-settings.ts new file mode 100644 index 0000000..a820d47 --- /dev/null +++ b/openprint/src/config/ai-settings.ts @@ -0,0 +1,79 @@ +/** + * AI 助手配置 —— 纯前端、零后端、存本地(localStorage)。 + * 走 OpenAI 兼容 /chat/completions 协议:用户自己填 地址 / Key / 模型 ID。 + * 与 print-settings.ts 同套路(window.localStorage + 固定 STORAGE_KEY)。 + */ +export interface AiSettings { + /** OpenAI 兼容的 baseURL,需含 /v1(如 https://api.openai.com/v1) */ + baseURL: string + /** 用户自己的 API Key(明文存本地,仅本地单用户场景) */ + apiKey: string + /** 模型 ID,如 gpt-4o-mini / deepseek-chat / qwen-plus */ + model: string + /** 是否已启用 AI 助手 */ + enabled: boolean +} + +/** 常见 provider 预设(点击即填,用户仍可自行改地址) */ +export interface AiProviderPreset { + label: string + baseURL: string + model: string +} + +export const AI_PROVIDER_PRESETS: AiProviderPreset[] = [ + { label: 'OpenAI', baseURL: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }, + { label: 'DeepSeek', baseURL: 'https://api.deepseek.com/v1', model: 'deepseek-chat' }, + { label: '通义千问', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' }, + { label: 'Moonshot', baseURL: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k' }, + { label: '自定义', baseURL: '', model: '' }, +] + +const STORAGE_KEY = 'openprint:ai:config' + +const DEFAULT_AI_SETTINGS: AiSettings = { + baseURL: 'https://api.openai.com/v1', + apiKey: '', + model: 'gpt-4o-mini', + enabled: false, +} + +function safeRead(): AiSettings { + if (typeof window === 'undefined') return { ...DEFAULT_AI_SETTINGS } + try { + const raw = window.localStorage.getItem(STORAGE_KEY) + if (!raw) return { ...DEFAULT_AI_SETTINGS } + const parsed = JSON.parse(raw) as Partial + return { + baseURL: typeof parsed.baseURL === 'string' ? parsed.baseURL : DEFAULT_AI_SETTINGS.baseURL, + apiKey: typeof parsed.apiKey === 'string' ? parsed.apiKey : '', + model: typeof parsed.model === 'string' ? parsed.model : DEFAULT_AI_SETTINGS.model, + enabled: !!parsed.enabled, + } + } catch { + return { ...DEFAULT_AI_SETTINGS } + } +} + +function safeWrite(v: AiSettings): void { + if (typeof window === 'undefined') return + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(v)) + } catch { + /* noop */ + } +} + +export function readAiSettings(): AiSettings { + return safeRead() +} + +export function writeAiSettings(v: AiSettings): void { + safeWrite(v) +} + +/** 是否已具备发起请求的最低配置 */ +export function isAiConfigured(): boolean { + const s = safeRead() + return s.enabled && !!s.baseURL.trim() && !!s.apiKey.trim() && !!s.model.trim() +} diff --git a/openprint/src/config/backend.ts b/openprint/src/config/backend.ts new file mode 100644 index 0000000..912f4a3 --- /dev/null +++ b/openprint/src/config/backend.ts @@ -0,0 +1,36 @@ +/** + * 后端对接配置开关 —— 《后端对接规范》§4 + * + * 铁律(2026-08-08 主任定):未配置后端接口时,全部走本地存储(模板)/ Mock(数据源), + * 一切功能不受影响。只有显式配置了 `VITE_OPENPRINT_API_BASE` 才切换为云端仓库。 + * + * 配置方式(项目根目录 .env): + * VITE_OPENPRINT_API_BASE=https://print.example.com + * VITE_OPENPRINT_API_TOKEN= # 可选,Bearer 鉴权 + * + * 基地址会被当作前缀拼接:`/api/print/templates` → `https://print.example.com/api/print/templates` + */ +import type { HttpOptions } from '@/repository/http-client' + +const API_BASE = (import.meta.env.VITE_OPENPRINT_API_BASE ?? '').trim() +const API_TOKEN = (import.meta.env.VITE_OPENPRINT_API_TOKEN ?? '').trim() + +export interface BackendConfig { + mode: 'cloud' + options: HttpOptions +} + +/** 是否已配置后端(决定走云端仓库还是本地/Mock) */ +export const isBackendConfigured = API_BASE.length > 0 + +/** 读取后端配置;未配置返回 null(调用方据此保持本地/Mock) */ +export function getBackendConfig(): BackendConfig | null { + if (!API_BASE) return null + return { + mode: 'cloud', + options: { + baseUrl: API_BASE, + token: API_TOKEN.length > 0 ? API_TOKEN : undefined, + }, + } +} diff --git a/openprint/src/config/print-settings.spec.ts b/openprint/src/config/print-settings.spec.ts new file mode 100644 index 0000000..8d828f6 --- /dev/null +++ b/openprint/src/config/print-settings.spec.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { + DEFAULT_PRINT_SETTINGS, + readPrintSettings, + writePrintSettings, + PRINT_SETTINGS_KEY, +} from '@/config/print-settings' + +describe('print-settings —— 打印设置读写', () => { + beforeEach(() => { + window.localStorage.clear() + }) + + it('无存储时返回默认值', () => { + expect(readPrintSettings()).toEqual(DEFAULT_PRINT_SETTINGS) + }) + + it('写入后可读回', () => { + const s = { + local: { + method: 'silent' as const, + silent: { host: '192.168.1.10', port: 9122 }, + copies: 3, + closeAfterPrint: false, + }, + remote: { enabled: true, host: 'http://10.0.0.8', port: 9200, printer: 'HP-1' }, + } + writePrintSettings(s) + expect(readPrintSettings()).toEqual(s) + }) + + it('损坏 JSON 回退默认值', () => { + window.localStorage.setItem(PRINT_SETTINGS_KEY, '{oops') + expect(readPrintSettings()).toEqual(DEFAULT_PRINT_SETTINGS) + }) + + it('部分配置深度合并(新增字段有默认值)', () => { + window.localStorage.setItem( + PRINT_SETTINGS_KEY, + JSON.stringify({ local: { method: 'silent' } }), + ) + const s = readPrintSettings() + expect(s.local.method).toBe('silent') + expect(s.local.copies).toBe(DEFAULT_PRINT_SETTINGS.local.copies) + expect(s.local.silent).toEqual({ host: '127.0.0.1', port: 18888 }) + expect(s.remote.enabled).toBe(false) + }) +}) diff --git a/openprint/src/config/print-settings.ts b/openprint/src/config/print-settings.ts new file mode 100644 index 0000000..b587d4f --- /dev/null +++ b/openprint/src/config/print-settings.ts @@ -0,0 +1,87 @@ +/** + * 打印设置 —— 本地打印 / 远程云打印 配置(全局设置,localStorage 持久化) + * 主任铁律:无后端全链路可用 —— 设置只存本地,绝不依赖后端。 + */ + +export interface LocalPrintConfig { + /** 打印方式:browser=浏览器打印对话框 / silent=客户端静默打印(需本地客户端服务) */ + method: 'browser' | 'silent' + /** + * 本地打印客户端连接地址。 + * 同时也是 /health、/printers、/print 三个接口的基地址来源, + * 解析优先级见 `@/config/printer` 的 resolvePrinterBaseUrl()。 + */ + silent: { + /** 客户端所在主机 IP(默认本机回环 127.0.0.1) */ + host: string + /** 客户端服务端口(默认 18888) */ + port: number + } + /** 副本数 */ + copies: number + /** 打印完成后自动关闭预览窗口 */ + closeAfterPrint: boolean +} + +export interface RemotePrintConfig { + /** 是否启用远程云打印 */ + enabled: boolean + /** 云打印服务地址(如 http://localhost 或 https://print.example.com) */ + host: string + /** 服务端口 */ + port: number + /** 目标打印机名(可选,留空使用服务默认打印机) */ + printer: string +} + +export interface PrintSettings { + local: LocalPrintConfig + remote: RemotePrintConfig +} + +export const PRINT_SETTINGS_KEY = 'openprint:print-settings' + +export const DEFAULT_PRINT_SETTINGS: PrintSettings = { + local: { + method: 'browser', + silent: { host: '127.0.0.1', port: 18888 }, + copies: 1, + closeAfterPrint: true, + }, + remote: { enabled: false, host: 'http://localhost', port: 9100, printer: '' }, +} + +/** 读取设置(损坏/缺失时回退默认值,深度合并保证新字段有默认) */ +export function readPrintSettings(): PrintSettings { + if (typeof window === 'undefined') return DEFAULT_PRINT_SETTINGS + try { + const raw = window.localStorage.getItem(PRINT_SETTINGS_KEY) + if (!raw) return DEFAULT_PRINT_SETTINGS + const parsed = JSON.parse(raw) as Partial + return { + local: { + ...DEFAULT_PRINT_SETTINGS.local, + ...(parsed.local ?? {}), + silent: { + ...DEFAULT_PRINT_SETTINGS.local.silent, + ...(parsed.local?.silent ?? {}), + }, + }, + remote: { + ...DEFAULT_PRINT_SETTINGS.remote, + ...(parsed.remote ?? {}), + }, + } + } catch { + return DEFAULT_PRINT_SETTINGS + } +} + +export function writePrintSettings(settings: PrintSettings): void { + if (typeof window === 'undefined') return + try { + window.localStorage.setItem(PRINT_SETTINGS_KEY, JSON.stringify(settings)) + } catch { + /* 存储不可用时静默失败(如隐私模式) */ + } +} diff --git a/openprint/src/config/printer.ts b/openprint/src/config/printer.ts new file mode 100644 index 0000000..9d6faf3 --- /dev/null +++ b/openprint/src/config/printer.ts @@ -0,0 +1,81 @@ +/** + * 打印客户端服务地址解析 —— 端口/地址覆盖的唯一出口 + * + * 出厂默认:http://127.0.0.1:18888 + * + * 覆盖优先级(高 → 低): + * 1. 用户在「设置 → 本地打印」里填的 IP/端口(localStorage `openprint:print-settings`) + * 2. 出厂默认 http://127.0.0.1:18888 + * + * 客户端已支持局域网打印:手填局域网地址(如 http://192.168.1.20:19000)即可直接推送, + * 手填地址优先级最高。 + * + * 主任铁律:无后端全链路可用 —— 这里只读本地存储与构建期常量,绝不请求任何后端。 + */ +import { DEFAULT_PRINTER_BASE_URL } from '@/core/print-client' +import { PRINT_SETTINGS_KEY } from './print-settings' + +/** 地址来源,UI 用于提示用户当前生效的是哪一层配置 */ +export type PrinterBaseSource = 'settings' | 'default' + +/** 出厂默认,供 UI 占位/重置使用 */ +export const FACTORY_PRINTER_BASE_URL = DEFAULT_PRINTER_BASE_URL +export const FACTORY_PRINTER_HOST = '127.0.0.1' +export const FACTORY_PRINTER_PORT = 18888 + +/** 补全协议 + 去掉尾部斜杠 */ +export function normalizePrinterBase(input: string): string { + const raw = input.trim().replace(/\/+$/, '') + if (!raw) return '' + return /^https?:\/\//i.test(raw) ? raw : `http://${raw}` +} + +/** host + port → 基地址。host 可带协议(http://x)或只写 IP/域名 */ +export function buildPrinterBase(host: string, port: number): string { + const base = normalizePrinterBase(host) + if (!base) return '' + // host 里已经自带端口就不再拼(如用户填了 "127.0.0.1:18888") + if (/:\d+$/.test(base)) return base + const p = Number.isFinite(port) && port > 0 ? Math.floor(port) : FACTORY_PRINTER_PORT + return `${base}:${p}` +} + +/** + * 读取用户在设置里显式配置的端点。 + * 只有 localStorage 里确实存在 local.silent.host/port 才算「用户配置过」, + * 否则返回 null 让位给环境变量。 + */ +export function readStoredPrinterEndpoint(): { host: string; port: number } | null { + if (typeof window === 'undefined') return null + try { + const raw = window.localStorage.getItem(PRINT_SETTINGS_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) as { + local?: { silent?: { host?: unknown; port?: unknown } } + } + const silent = parsed?.local?.silent + if (!silent) return null + const host = typeof silent.host === 'string' ? silent.host.trim() : '' + const port = typeof silent.port === 'number' ? silent.port : Number(silent.port) + if (!host || !Number.isFinite(port) || port <= 0) return null + return { host, port: Math.floor(port) } + } catch { + return null + } +} + +/** 当前生效的打印客户端基地址 */ +export function resolvePrinterBaseUrl(): string { + const stored = readStoredPrinterEndpoint() + if (stored) { + const base = buildPrinterBase(stored.host, stored.port) + if (base) return base + } + return FACTORY_PRINTER_BASE_URL +} + +/** 当前地址来自哪一层配置(UI 提示用) */ +export function resolvePrinterBaseSource(): PrinterBaseSource { + if (readStoredPrinterEndpoint()) return 'settings' + return 'default' +} diff --git a/openprint/src/core/__tests__/cjk-measurer.ts b/openprint/src/core/__tests__/cjk-measurer.ts new file mode 100644 index 0000000..b387b83 --- /dev/null +++ b/openprint/src/core/__tests__/cjk-measurer.ts @@ -0,0 +1,34 @@ +import type { TextMeasureOptions, TextMeasurer } from '@/core/layout-engine/measure' + +/** + * 测试用确定性 CJK 估算测量器。 + * + * 不依赖任何 DOM(happy-dom 不实现真实 CSS 布局),用字符宽度估算, + * 保证分页单测在 CI / node 下结果稳定、可复现。 + * 估算规则:CJK 字符 ≈ 1em 宽,ASCII ≈ 0.6em 宽;行高 = lineHeight × fontSize(pt→mm)。 + */ +export function createCjkMeasurer(): TextMeasurer { + return { + measure(text: string, o: TextMeasureOptions) { + const fsMm = (o.fontSize * 25.4) / 72 // pt → mm + const lineH = (o.lineHeight ?? 1.35) * fsMm + const charW = fsMm // CJK 近似 1em + const asciiW = fsMm * 0.6 + const lines = text.split('\n') + let totalH = 0 + let lineCount = 0 + for (const ln of lines) { + let w = 0 + for (const ch of ln) w += /[\x00-\xff]/.test(ch) ? asciiW : charW + const wrapped = + o.widthMm > 0 && o.wrap !== false ? Math.max(1, Math.ceil(w / o.widthMm)) : 1 + totalH += wrapped * lineH + lineCount += wrapped + } + return { heightMm: totalH, lines: lineCount } + }, + dispose() { + /* 无资源可释放 */ + }, + } +} diff --git a/openprint/src/core/chartkit/bar.ts b/openprint/src/core/chartkit/bar.ts new file mode 100644 index 0000000..3df37d9 --- /dev/null +++ b/openprint/src/core/chartkit/bar.ts @@ -0,0 +1,126 @@ +/** + * chartkit/bar —— 垂直分组条形图(零依赖 SVG) + * + * 输出一串 (按序列分组并排)+ 坐标轴 + 可选网格 + 图例 + 数据标签。 + * 全部用逻辑坐标(viewBox),实际尺寸由外层容器缩放,矢量清晰。 + */ +import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX } from './core' +import type { ChartModel } from './types' + +export function renderBar(model: ChartModel): string { + const opt = model.options ?? {} + const W = opt.width ?? 480 + const H = opt.height ?? 320 + const title = opt.title + const showAxis = opt.showAxis ?? true + const showGrid = opt.showGrid ?? true + const showLegend = opt.showLegend ?? model.series.length > 1 + const valueLabel = opt.valueLabel ?? false + + const legendH = showLegend ? 22 : 0 + const titleH = title ? 22 : 0 + const mTop = 10 + titleH + const mRight = 16 + const mBottom = (showAxis ? 42 : 22) + legendH + const mLeft = showAxis ? 44 : 16 + + const px0 = mLeft + const py0 = mTop + const px1 = W - mRight + const py1 = H - mBottom + const plotW = Math.max(1, px1 - px0) + const plotH = Math.max(1, py1 - py0) + + const cats = model.categories.length + ? model.categories + : (model.series[0]?.data ?? []).map((_, i) => String(i + 1)) + const n = cats.length + + const allVals: number[] = [] + for (const s of model.series) for (const v of s.data) if (isFinite(v)) allVals.push(v) + if (model.series.length === 0 || n === 0 || allVals.length === 0) { + return placeholder(W, H, '暂无数据') + } + + const yMax = niceMax(Math.max(0, ...allVals)) + const band = plotW / Math.max(1, n) + const inner = band * 0.74 + const sCount = model.series.length + const barW = inner / Math.max(1, sCount) + + const parts: string[] = [] + parts.push(``) + + if (title) { + parts.push(`${escapeXml(title)}`) + } + + // 网格 + y 轴刻度 + if (showAxis) { + const ticks = 4 + for (let i = 0; i <= ticks; i++) { + const v = (yMax / ticks) * i + const y = py1 - (v / yMax) * plotH + if (showGrid && i > 0) { + parts.push(``) + } + parts.push(`${fmt(v)}`) + } + // 基线 + parts.push(``) + } + + // 柱体 + for (let c = 0; c < n; c++) { + const cx = px0 + band * (c + 0.5) + const groupX0 = cx - inner / 2 + for (let s = 0; s < sCount; s++) { + const val = model.series[s]!.data[c] ?? 0 + if (!isFinite(val)) continue + const h = (Math.max(0, val) / yMax) * plotH + const x = groupX0 + s * barW + const y = py1 - h + const color = seriesColor(model.series[s]!, s, opt) + parts.push(``) + if (valueLabel && h > 0) { + parts.push(`${fmt(val)}`) + } + } + // x 轴标签:始终以类目为基准居中于对应条带正下方,并与图表留一点间距 + if (showAxis) { + parts.push(`${escapeXml(truncate(String(cats[c] ?? '')))}`) + } + } + + // 图例 + if (showLegend) { + const align = opt.labelAlign ?? 'center' + let totalW = 0 + for (let s = 0; s < sCount; s++) { + const name = model.series[s]!.name || `系列${s + 1}` + totalW += 16 + name.length * 12 + 18 + } + const startX = legendStartX(align, W, totalW, px0) + let lx = startX + let ly = py1 + (showAxis ? 40 : 16) + for (let s = 0; s < sCount; s++) { + const name = model.series[s]!.name || `系列${s + 1}` + const color = seriesColor(model.series[s]!, s, opt) + const itemW = 16 + name.length * 12 + 18 + if (lx + itemW > W - 8 && lx > startX) { + lx = startX + ly += 16 + } + parts.push(``) + parts.push(`${escapeXml(truncate(name, 10))}`) + lx += itemW + } + } + + parts.push('') + return parts.join('') +} + +function placeholder(W: number, H: number, text: string): string { + return `${escapeXml(text)}` +} diff --git a/openprint/src/core/chartkit/chartkit.spec.ts b/openprint/src/core/chartkit/chartkit.spec.ts new file mode 100644 index 0000000..10f8149 --- /dev/null +++ b/openprint/src/core/chartkit/chartkit.spec.ts @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest' +import { + renderChartSvg, + chartControlToModel, + renderChartControl, + DEFAULT_PALETTE, +} from './index' +import type { ChartModel } from './types' + +const barModel: ChartModel = { + kind: 'bar', + categories: ['一月', '二月', '三月'], + series: [ + { name: '销量', data: [12, 19, 8] }, + { name: '退货', data: [3, 5, 2] }, + ], +} + +const lineModel: ChartModel = { + kind: 'line', + categories: ['周一', '周二', '周三'], + series: [{ name: '温度', data: [20, 25, 22] }], +} + +const pieModel: ChartModel = { + kind: 'pie', + categories: ['苹果', '香蕉', '橙子'], + series: [{ name: '销量', data: [10, 20, 30] }], +} + +function count(haystack: string, needle: string): number { + return haystack.split(needle).length - 1 +} + +describe('chartkit · 产出结构', () => { + it('三种图表都返回以 结尾的可缩放 SVG', () => { + for (const m of [barModel, lineModel, pieModel]) { + const svg = renderChartSvg(m) + expect(svg.startsWith('')).toBe(true) + expect(svg).toContain('viewBox="0 0') + expect(svg).toContain('width="100%"') + } + }) + + it('bar 为每个(序列×类目)生成一个 ', () => { + const svg = renderChartSvg(barModel) + // 2 序列 × 3 类目 = 6 根柱(柱体 rx=1.5,区别于图例色块 rx=2) + expect(count(svg, 'rx="1.5"')).toBe(6) + }) + + it('line 含一条折线路径 + 每个点一个圆点', () => { + const svg = renderChartSvg(lineModel) + expect(count(svg, '', () => { + const svg = renderChartSvg(pieModel) + expect(count(svg, ' { + const normal = renderChartSvg(pieModel) + const donut = renderChartSvg({ ...pieModel, options: { donut: true } }) + // 3 个扇区:普通每个 1 段弧;环形每个 2 段弧(外圈+内圈) + expect(count(normal, ' A ')).toBe(3) + expect(count(donut, ' A ')).toBe(6) + }) + + it('饼图实心扇区必须到达圆心(修复"中间空白")', () => { + const normal = renderChartSvg(pieModel) + const donut = renderChartSvg({ ...pieModel, options: { donut: true } }) + // 默认 480×320 无标题无环形:圆心 (240,148),楔形路径应以 "M 240.00 148.00" 起笔 + expect(normal).toContain('M 240.00 148.00') + // 环形是环段,不应从圆心起笔 + expect(donut).not.toContain('M 240.00 148.00') + }) +}) + +describe('chartkit · 空/异常数据兜底', () => { + it('空数据渲染占位提示,不抛错', () => { + const svg = renderChartSvg({ kind: 'bar', categories: [], series: [] }) + expect(svg).toContain('暂无数据') + }) + + it('饼图总量为 0 时不画扇区', () => { + const svg = renderChartSvg({ kind: 'pie', categories: ['a', 'b'], series: [{ name: 's', data: [0, 0] }] }) + expect(svg).toContain('暂无数据') + }) +}) + +describe('chartkit · 文本转义', () => { + it('标题中的 & < > 被转义,不会破坏 SVG 结构', () => { + const svg = renderChartSvg({ ...barModel, options: { title: 'A & B "D"' } }) + expect(svg).not.toContain('A & B') + expect(svg).toContain('A & B <C>') + }) +}) + +describe('chartkit · ChartControl 映射', () => { + it('chartControlToModel 补齐缺省并过滤非有限值', () => { + const m = chartControlToModel({ + type: 'chart', + id: 'c1', + kind: 'line', + left: 0, + top: 0, + width: 60, + height: 40, + categories: ['x', 'y'], + series: [{ name: 's', data: [1, NaN, 3] }], + }) + expect(m.kind).toBe('line') + expect(m.series[0]!.data).toEqual([1, 3]) + expect(m.categories).toEqual(['x', 'y']) + }) + + it('renderChartControl 从协议对象直接出 SVG', () => { + const svg = renderChartControl({ + type: 'chart', + id: 'c2', + kind: 'pie', + left: 0, + top: 0, + width: 50, + height: 50, + categories: ['a', 'b'], + series: [{ name: 's', data: [1, 2] }], + }) + expect(svg).toContain(' 破坏 SVG 结构) */ +export function escapeXml(s: string): string { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +/** 数值格式化:整数去小数,小数保留最多 2 位 */ +export function fmt(n: number): string { + if (!isFinite(n)) return '' + const r = Math.round(n * 100) / 100 + return Number.isInteger(r) ? String(r) : String(r) +} + +/** + * 计算「好看」的坐标轴上限:取数据最大值,向上取整到 1/2/5×10ⁿ 量级。 + * 例如 max=73 → 80;max=250 → 300;max=0 → 1。 + */ +export function niceMax(max: number): number { + if (max <= 0) return 1 + const exp = Math.floor(Math.log10(max)) + const base = Math.pow(10, exp) + const frac = max / base + let nice: number + if (frac <= 1) nice = 1 + else if (frac <= 2) nice = 2 + else if (frac <= 5) nice = 5 + else nice = 10 + return nice * base +} + +/** 取序列颜色:显式 color 优先,否则按调色板顺序取 */ +export function seriesColor(series: ChartSeries, index: number, options?: ChartOptions): string { + if (series.color) return series.color + const palette = options?.palette ?? DEFAULT_PALETTE + return palette[index % palette.length]! +} + +/** + * 极坐标 → 笛卡尔坐标。angleDeg 以「12 点方向为 0°、顺时针递增」表示, + * 与饼图扇区习惯一致。 + */ +export function polar(cx: number, cy: number, r: number, angleDeg: number): { x: number; y: number } { + const theta = ((angleDeg - 90) * Math.PI) / 180 + return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) } +} + +/** + * 饼图扇区路径(支持环形 donut)。 + * @param startAngle 起始角(度,顺时针从 12 点起) + * @param endAngle 结束角(度) + * @param rInner 内半径(donut>0 时 >0) + */ +export function arcPath( + cx: number, + cy: number, + rOuter: number, + startAngle: number, + endAngle: number, + rInner = 0, +): string { + const largeArc = endAngle - startAngle > 180 ? 1 : 0 + // 整圆特例:单扇区 360° 用两段半圆弧画,否则 A 命令会因起止点重合而失效 + if (endAngle - startAngle >= 359.999) { + const top = polar(cx, cy, rOuter, 0) + const bottom = polar(cx, cy, rOuter, 180) + let d = `M ${cx.toFixed(2)} ${cy.toFixed(2)} L ${top.x.toFixed(2)} ${top.y.toFixed(2)} A ${rOuter} ${rOuter} 0 1 1 ${bottom.x.toFixed(2)} ${bottom.y.toFixed(2)} A ${rOuter} ${rOuter} 0 1 1 ${top.x.toFixed(2)} ${top.y.toFixed(2)} Z` + if (rInner > 0) { + const itop = polar(cx, cy, rInner, 0) + const ibottom = polar(cx, cy, rInner, 180) + d += ` M ${cx.toFixed(2)} ${cy.toFixed(2)} L ${itop.x.toFixed(2)} ${itop.y.toFixed(2)} A ${rInner} ${rInner} 0 1 0 ${ibottom.x.toFixed(2)} ${ibottom.y.toFixed(2)} A ${rInner} ${rInner} 0 1 0 ${itop.x.toFixed(2)} ${itop.y.toFixed(2)} Z` + } + return d + } + const start = polar(cx, cy, rOuter, startAngle) + const end = polar(cx, cy, rOuter, endAngle) + // 环形:外弧(顺时针) + 内弧(逆时针) 组成闭合环段(不连圆心) + if (rInner > 0) { + const iStart = polar(cx, cy, rInner, startAngle) + const iEnd = polar(cx, cy, rInner, endAngle) + return ( + `M ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)}` + + ` L ${iEnd.x.toFixed(2)} ${iEnd.y.toFixed(2)} A ${rInner} ${rInner} 0 ${largeArc} 0 ${iStart.x.toFixed(2)} ${iStart.y.toFixed(2)} Z` + ) + } + // 实心饼:圆心 → 外起点 → 弧 → 圆心,形成真正到达圆心的楔形(修复"中间空白") + return `M ${cx.toFixed(2)} ${cy.toFixed(2)} L ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)} Z` +} + +/** 截断过长标签(设计期/运行期共用) */ +export function truncate(s: string, max = 12): string { + return s.length > max ? s.slice(0, max - 1) + '…' : s +} + +/** + * 图例整体起始 x:left 直接用 leftPad;center/right 按单行总宽偏移。 + * 单行假设(多数图例一行放得下);若实际换行,仍从 startX 起排,整体观感一致。 + */ +export function legendStartX( + align: 'left' | 'center' | 'right' | undefined, + W: number, + totalW: number, + leftPad: number, +): number { + if (align === 'center') return Math.max(leftPad, (W - totalW) / 2) + if (align === 'right') return Math.max(leftPad, W - totalW - 8) + return leftPad +} diff --git a/openprint/src/core/chartkit/index.ts b/openprint/src/core/chartkit/index.ts new file mode 100644 index 0000000..b67a24d --- /dev/null +++ b/openprint/src/core/chartkit/index.ts @@ -0,0 +1,54 @@ +/** + * chartkit/index —— 对外统一入口 + * + * - `renderChartSvg(model)`:纯函数,ChartModel → 可缩放 SVG 字符串。 + * - `chartControlToModel(control)`:把设计器里的 ChartControl 协议对象转成 ChartModel + * (补默认值、剔除非法字段),供设计期 overlay 与运行期导出共用。 + * - `renderChartControl(control)`:一步到位,从 ChartControl 直接出 SVG 字符串。 + * + * 整个模块零依赖、不碰 DOM,可在 Node / 测试环境运行。 + */ +import type { ChartControl } from '../../types/control' +import type { ChartKind, ChartModel, ChartSeries } from './types' +import { renderBar } from './bar' +import { renderLine } from './line' +import { renderPie } from './pie' + +export type { ChartKind, ChartModel, ChartSeries, ChartOptions } from './types' +export { DEFAULT_PALETTE } from './types' +export { renderBar } from './bar' +export { renderLine } from './line' +export { renderPie } from './pie' + +export function renderChartSvg(model: ChartModel): string { + switch (model.kind) { + case 'bar': + return renderBar(model) + case 'line': + return renderLine(model) + case 'pie': + return renderPie(model) + default: + return '' + } +} + +/** ChartControl(协议)→ ChartModel(渲染器输入),补齐缺省值 */ +export function chartControlToModel(c: ChartControl): ChartModel { + const series: ChartSeries[] = (c.series ?? []).map((s) => ({ + name: s.name ?? '', + data: Array.isArray(s.data) ? s.data.filter((v) => typeof v === 'number' && isFinite(v)) : [], + color: s.color, + })) + return { + kind: c.kind, + categories: Array.isArray(c.categories) ? c.categories.map(String) : [], + series, + options: c.options, + } +} + +/** 一步到位:ChartControl → 可缩放 SVG 字符串 */ +export function renderChartControl(control: ChartControl): string { + return renderChartSvg(chartControlToModel(control)) +} diff --git a/openprint/src/core/chartkit/line.ts b/openprint/src/core/chartkit/line.ts new file mode 100644 index 0000000..315657c --- /dev/null +++ b/openprint/src/core/chartkit/line.ts @@ -0,0 +1,164 @@ +/** + * chartkit/line —— 折线图(零依赖 SVG) + * + * 多序列折线 + 坐标轴 + 可选网格 + 图例 + 数据标签。 + * 支持 smooth(Catmull-Rom 平滑曲线)与 area(折线下方面积填充)。 + */ +import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX } from './core' +import type { ChartModel } from './types' + +interface Pt { x: number; y: number } + +function smoothPath(pts: Pt[]): string { + if (pts.length < 2) return pts.length ? `M ${pts[0]!.x.toFixed(1)} ${pts[0]!.y.toFixed(1)}` : '' + let d = `M ${pts[0]!.x.toFixed(1)} ${pts[0]!.y.toFixed(1)}` + const t = 1 / 6 + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[i - 1] ?? pts[i]! + const p1 = pts[i]! + const p2 = pts[i + 1]! + const p3 = pts[i + 2] ?? p2 + const c1x = p1.x + (p2.x - p0.x) * t + const c1y = p1.y + (p2.y - p0.y) * t + const c2x = p2.x - (p3.x - p1.x) * t + const c2y = p2.y - (p3.y - p1.y) * t + d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}` + } + return d +} + +export function renderLine(model: ChartModel): string { + const opt = model.options ?? {} + const W = opt.width ?? 480 + const H = opt.height ?? 320 + const title = opt.title + const showAxis = opt.showAxis ?? true + const showGrid = opt.showGrid ?? true + const showLegend = opt.showLegend ?? model.series.length > 1 + const valueLabel = opt.valueLabel ?? false + const smooth = opt.smooth ?? false + const area = opt.area ?? false + + const legendH = showLegend ? 22 : 0 + const titleH = title ? 22 : 0 + const mTop = 10 + titleH + const mRight = 16 + const mBottom = (showAxis ? 42 : 22) + legendH + const mLeft = showAxis ? 44 : 16 + + const px0 = mLeft + const py0 = mTop + const px1 = W - mRight + const py1 = H - mBottom + const plotW = Math.max(1, px1 - px0) + const plotH = Math.max(1, py1 - py0) + + const cats = model.categories.length + ? model.categories + : (model.series[0]?.data ?? []).map((_, i) => String(i + 1)) + const n = cats.length + + const allVals: number[] = [] + for (const s of model.series) for (const v of s.data) if (isFinite(v)) allVals.push(v) + if (model.series.length === 0 || n === 0 || allVals.length === 0) { + return placeholder(W, H, '暂无数据') + } + + const yMax = niceMax(Math.max(0, ...allVals)) + const band = plotW / Math.max(1, n) + + // 预计算每序列的每个点 + const seriesPts = model.series.map((s, si) => { + const color = seriesColor(s, si, opt) + const pts: Pt[] = [] + for (let c = 0; c < n; c++) { + const val = s.data[c] ?? 0 + const x = n > 1 ? px0 + band * (c + 0.5) : (px0 + px1) / 2 + const y = py1 - (Math.max(0, val) / yMax) * plotH + pts.push({ x, y }) + } + return { color, pts, name: s.name } + }) + + const parts: string[] = [] + parts.push(``) + + if (title) { + parts.push(`${escapeXml(title)}`) + } + + if (showAxis) { + const ticks = 4 + for (let i = 0; i <= ticks; i++) { + const v = (yMax / ticks) * i + const y = py1 - (v / yMax) * plotH + if (showGrid && i > 0) { + parts.push(``) + } + parts.push(`${fmt(v)}`) + } + parts.push(``) + parts.push(``) + for (let c = 0; c < n; c++) { + const x = px0 + band * (c + 0.5) + // x 轴类目标签:始终以类目为基准居中于对应点正下方,并与图表留一点间距 + parts.push(`${escapeXml(truncate(String(cats[c] ?? '')))}`) + } + } + + for (const sp of seriesPts) { + if (area && sp.pts.length > 1) { + let d = smooth ? smoothPath(sp.pts) : `M ${sp.pts.map((p) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' L ')}` + const last = sp.pts[sp.pts.length - 1]! + const first = sp.pts[0]! + d += ` L ${last.x.toFixed(1)} ${py1} L ${first.x.toFixed(1)} ${py1} Z` + parts.push(``) + } + const lineD = smooth ? smoothPath(sp.pts) : (sp.pts.length ? `M ${sp.pts.map((p) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' L ')}` : '') + if (lineD) parts.push(``) + for (const p of sp.pts) { + parts.push(``) + } + } + if (valueLabel) { + for (let si = 0; si < seriesPts.length; si++) { + const sp = seriesPts[si]! + const series = model.series[si]! + for (let c = 0; c < sp.pts.length; c++) { + const v = series.data[c] ?? 0 + parts.push(`${fmt(v)}`) + } + } + } + + if (showLegend) { + const align = opt.labelAlign ?? 'center' + let totalW = 0 + for (let s = 0; s < seriesPts.length; s++) { + const name = seriesPts[s]!.name || `系列${s + 1}` + totalW += 16 + name.length * 12 + 18 + } + const startX = legendStartX(align, W, totalW, px0) + let lx = startX + let ly = py1 + (showAxis ? 40 : 16) + for (let s = 0; s < seriesPts.length; s++) { + const name = seriesPts[s]!.name || `系列${s + 1}` + const color = seriesPts[s]!.color + const itemW = 16 + name.length * 12 + 18 + if (lx + itemW > W - 8 && lx > startX) { + lx = startX + ly += 16 + } + parts.push(``) + parts.push(`${escapeXml(truncate(name, 10))}`) + lx += itemW + } + } + + parts.push('') + return parts.join('') +} + +function placeholder(W: number, H: number, text: string): string { + return `${escapeXml(text)}` +} diff --git a/openprint/src/core/chartkit/pie.ts b/openprint/src/core/chartkit/pie.ts new file mode 100644 index 0000000..3fe9cfd --- /dev/null +++ b/openprint/src/core/chartkit/pie.ts @@ -0,0 +1,100 @@ +/** + * chartkit/pie —— 饼图 / 环形图(零依赖 SVG) + * + * 用 arcPath 画每个扇区(或环形),标签显示类目+百分比,底部可选图例。 + * 饼图通常只用 series[0](每个数据点 = 一个扇区)。 + */ +import { arcPath, escapeXml, seriesColor, truncate, legendStartX } from './core' +import { DEFAULT_PALETTE } from './types' +import type { ChartModel } from './types' + +export function renderPie(model: ChartModel): string { + const opt = model.options ?? {} + const W = opt.width ?? 480 + const H = opt.height ?? 320 + const title = opt.title + const showLegend = opt.showLegend ?? true + const donut = opt.donut ?? false + const valueLabel = opt.valueLabel ?? true + + const legendH = showLegend ? 24 : 0 + const titleH = title ? 22 : 0 + + const cats = model.categories.length + ? model.categories + : (model.series[0]?.data ?? []).map((_, i) => `类目${i + 1}`) + const data = (model.series[0]?.data ?? []).filter((v) => isFinite(v)) + const total = data.reduce((a, b) => a + Math.max(0, b), 0) + if (data.length === 0 || total <= 0) { + return placeholder(W, H, '暂无数据') + } + + const cx = W / 2 + const cy = titleH + (H - titleH - legendH) / 2 + const rOuter = (Math.min(W * 0.82, H - titleH - legendH) / 2) * 0.92 + const rInner = donut ? rOuter * 0.56 : 0 + + const palette = opt.palette ?? DEFAULT_PALETTE + const parts: string[] = [] + parts.push(``) + + if (title) { + parts.push(`${escapeXml(title)}`) + } + + let angle = 0 + for (let i = 0; i < data.length; i++) { + const v = Math.max(0, data[i]!) + if (v <= 0) continue + const sweep = (v / total) * 360 + const start = angle + const end = angle + sweep + angle = end + const color = seriesColor({ name: cats[i] ?? '', data: [] }, i, opt) + parts.push(``) + // 百分比标签 + if (valueLabel && sweep > 12) { + const mid = (start + end) / 2 + const lr = donut ? (rOuter + rInner) / 2 : rOuter * 0.62 + const p = polarL(lr, mid) + const pct = ((v / total) * 100).toFixed(0) + '%' + parts.push(`${pct}`) + } + } + + if (showLegend) { + const align = opt.labelAlign ?? 'center' + let totalW = 0 + for (let i = 0; i < data.length; i++) { + const name = cats[i] ?? `类目${i + 1}` + totalW += 16 + name.length * 12 + 18 + } + const startX = legendStartX(align, W, totalW, 12) + let lx = startX + let ly = H - legendH + 12 + for (let i = 0; i < data.length; i++) { + const name = cats[i] ?? `类目${i + 1}` + const color = seriesColor({ name, data: [] }, i, opt) + const itemW = 16 + name.length * 12 + 18 + if (lx + itemW > W - 8 && lx > startX) { + lx = startX + ly += 16 + } + parts.push(``) + parts.push(`${escapeXml(truncate(name, 10))}`) + lx += itemW + } + } + + parts.push('') + return parts.join('') + + function polarL(r: number, angleDeg: number) { + const theta = ((angleDeg - 90) * Math.PI) / 180 + return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) } + } +} + +function placeholder(W: number, H: number, text: string): string { + return `${escapeXml(text)}` +} diff --git a/openprint/src/core/chartkit/types.ts b/openprint/src/core/chartkit/types.ts new file mode 100644 index 0000000..00430d9 --- /dev/null +++ b/openprint/src/core/chartkit/types.ts @@ -0,0 +1,72 @@ +/** + * chartkit —— OpenPrint 原生 SVG 图表组件(零依赖) + * + * 设计目标(见《OpenPrint-设计方案》图表控件提案): + * - 打印设计器要的是**矢量**,不是 Canvas 位图。图表用纯 SVG 路径绘制, + * 导出 PDF/SVG 时整条链路都是矢量,任意缩放都清晰,且零运行时依赖。 + * - 本模块是**纯函数 + 零依赖**的:输入 ChartModel,输出一段可缩放的 `` 字符串。 + * 设计期 overlay 与运行期导出共用同一份产物,保证"设计即打印"。 + * + * 支持的图表类型(v1):条形图 bar / 折线图 line / 饼图 pie。 + * 数据模型与渲染解耦,后续扩展(横向条形、面积、环形、堆叠等)只需加一个生成器。 + */ + +export type ChartKind = 'bar' | 'line' | 'pie' + +/** 一条数据序列(折线/条形的一组柱;饼图通常只用 series[0]) */ +export interface ChartSeries { + name: string + /** 与 ChartModel.categories 一一对应 */ + data: number[] + /** 可选覆盖色;缺省走调色板 */ + color?: string +} + +/** 图表下方标签(x 轴类目 / 图例)的水平对齐方式 */ +export type ChartLabelAlign = 'left' | 'center' | 'right' + +export interface ChartOptions { + /** SVG 逻辑尺寸(px,仅决定 viewBox 比例;实际渲染按容器缩放)。默认 480×320 */ + width?: number + height?: number + /** 图表主标题(绘制在顶部居中) */ + title?: string + /** 显示图例(多序列时建议开) */ + showLegend?: boolean + /** 显示坐标轴(条形/折线生效;饼图忽略) */ + showAxis?: boolean + /** 显示网格线(条形/折线生效) */ + showGrid?: boolean + /** 调色板(序列缺色时按顺序取) */ + palette?: string[] + /** 折线是否平滑曲线 */ + smooth?: boolean + /** 折线下方填充面积 */ + area?: boolean + /** 饼图环形(中心镂空) */ + donut?: boolean + /** 数据标签(柱顶数值 / 饼图百分比) */ + valueLabel?: boolean + /** 下方标签(x 轴类目 / 图例)水平对齐,默认 left */ + labelAlign?: ChartLabelAlign +} + +export interface ChartModel { + kind: ChartKind + /** 类目轴标签(条形/折线为 x 轴;饼图为扇区名) */ + categories: string[] + series: ChartSeries[] + options?: ChartOptions +} + +/** 默认调色板(参考 AntV / ECharts 经典商务色,打印对比度足够) */ +export const DEFAULT_PALETTE = [ + '#5B8FF9', + '#5AD8A6', + '#5D7092', + '#F6BD16', + '#E8684A', + '#6DC8EC', + '#9270CA', + '#FF9D4D', +] diff --git a/openprint/src/core/export-engine/chart-svg-to-path.ts b/openprint/src/core/export-engine/chart-svg-to-path.ts new file mode 100644 index 0000000..c48bd70 --- /dev/null +++ b/openprint/src/core/export-engine/chart-svg-to-path.ts @@ -0,0 +1,85 @@ +/** + * chart-svg-to-path —— 把图表 SVG 里的 转成矢量 字形轮廓 + * + * 背景:jsPDF 只能内嵌 TrueType(glyf) 字体,无法内嵌 CFF/OpenType 字体; + * 思源宋体 .ttf 实为 CFF,jsPDF 内嵌会静默失败 → 矢量图表中文标签变空白。 + * 解决:在导出 PDF 的矢量路径里,用 opentype.js 把每个字形展开为 path, + * 文本即成为与字体无关的矢量轮廓,svg2pdf 直接画 path,中文不再丢。 + * + * 仅 export-pdf 的图表矢量路径按需动态调用,不进主包、不影响设计期/预览。 + */ +const SERIF_TTF_URL = '/fonts/SourceHanSerifCN-Regular.ttf' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let fontPromise: Promise | null = null + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function loadFont(): Promise { + if (!fontPromise) { + fontPromise = (async () => { + try { + const res = await fetch(SERIF_TTF_URL) + if (!res.ok) return null + const buf = await res.arrayBuffer() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ot: any = await import('opentype.js') + const parse = ot.parse ?? ot.default?.parse ?? ot.default + return parse(buf) + } catch { + return null + } + })() + } + return fontPromise +} + +/** + * 把图表 SVG 字符串里的所有 替换为字形轮廓 。 + * 字体加载失败/解析异常时原样返回(交给 svg2pdf,最坏情况该页走栅格兜底)。 + */ +export async function outlineChartSvgText(svg: string): Promise { + const font = await loadFont() + if (!font) return svg + + const doc = new DOMParser().parseFromString(svg, 'image/svg+xml') + const svgEl = doc.documentElement + if (!svgEl || svgEl.nodeName.toLowerCase() !== 'svg') return svg + + const texts = Array.from(svgEl.getElementsByTagName('text')) + for (const t of texts) { + const text = t.textContent ?? '' + if (!text) { + t.remove() + continue + } + const x = parseFloat(t.getAttribute('x') ?? '0') || 0 + const y = parseFloat(t.getAttribute('y') ?? '0') || 0 + const fontSize = parseFloat(t.getAttribute('font-size') ?? '10') || 10 + const fill = t.getAttribute('fill') || '#000000' + const anchor = (t.getAttribute('text-anchor') || 'start').toLowerCase() + + const scale = fontSize / font.unitsPerEm + const total = font.getAdvanceWidth(text, fontSize) + let startX = x + if (anchor === 'middle') startX = x - total / 2 + else if (anchor === 'end') startX = x - total + + const ds: string[] = [] + let cursor = startX + for (const ch of text) { + const g = font.charToGlyph(ch) + if (!g) continue + const gp = g.getPath(cursor, y, fontSize) + const d = gp.toPathData(2) + if (d && d.trim()) ds.push(d) + cursor += (g.advanceWidth || 0) * scale + } + + const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path') + path.setAttribute('d', ds.join(' ')) + path.setAttribute('fill', fill) + if (t.parentNode) t.parentNode.replaceChild(path, t) + } + + return new XMLSerializer().serializeToString(svgEl) +} diff --git a/openprint/src/core/export-engine/export-image.ts b/openprint/src/core/export-engine/export-image.ts new file mode 100644 index 0000000..7c3f385 --- /dev/null +++ b/openprint/src/core/export-engine/export-image.ts @@ -0,0 +1,13 @@ +/** + * 位图导出(JPG)—— 基于 rasterize 的单页位图 + */ +import type { LayoutResult } from '@/core/layout-engine/types' +import { pageToImageBlob, type PageImageOptions } from './rasterize' + +export function pageToJpg( + result: LayoutResult, + index: number, + opts: PageImageOptions = {}, +): Promise { + return pageToImageBlob(result, index, { type: 'image/jpeg', ...opts }) +} diff --git a/openprint/src/core/export-engine/export-pdf.spec.ts b/openprint/src/core/export-engine/export-pdf.spec.ts new file mode 100644 index 0000000..e8594e8 --- /dev/null +++ b/openprint/src/core/export-engine/export-pdf.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from 'vitest' + +// 真实环境无法在 happy-dom 中栅格化 SVG foreignObject,也不应让 jsPDF 读本地文件。 +// 这里 mock 栅格化下层 + Blob→dataURL,只验证 documentToPdf 的 jsPDF 装配。 +// (写死思源宋体的内联逻辑在 export-pdf.ts 内由 PDF_FONTS 常量体现,随代码审查保证。) +vi.mock('@/core/export-engine/rasterize', () => ({ + pageToImageBlob: vi.fn(async () => { + const b64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + const bin = atob(b64) + const bytes = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) + return new Blob([bytes], { type: 'image/png' }) + }), +})) + +vi.mock('@/core/export-engine/util', () => ({ + blobToDataURL: vi.fn(async () => { + // 直接返回合法 PNG data URI,避开 happy-dom 的 FileReader/fs-read 限制 + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + }), +})) + +import { documentToPdf } from '@/core/export-engine/export-pdf' +import { layout } from '@/core/layout-engine/pagination-engine' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price: 10, + amount: qty * 10, + } + }) + return { order: { orderNo: 'SO-001' }, customer: { name: '客户' }, items } +} + +describe('documentToPdf —— 位图 PDF 装配(jsPDF)', () => { + it('多页模板产出单文件多页 PDF blob', async () => { + const result = await layout(createDemoTemplate(), makeData(30), { measurer: createCjkMeasurer() }) + const blob = await documentToPdf(result, { scale: 2 }) + expect(blob).toBeInstanceOf(Blob) + expect(blob.type).toBe('application/pdf') + expect(blob.size).toBeGreaterThan(0) + }) +}) diff --git a/openprint/src/core/export-engine/export-pdf.ts b/openprint/src/core/export-engine/export-pdf.ts new file mode 100644 index 0000000..1b6429b --- /dev/null +++ b/openprint/src/core/export-engine/export-pdf.ts @@ -0,0 +1,208 @@ +/** + * PDF 导出 —— jsPDF 动态 import(不进主包) + * + * ## 位图底图默认 PNG(无损 · 最清晰) + * 早期为压体积改过 JPEG,但纯文字/线条在白底上即使 quality=1 也会被 8×8 DCT + * 引入极轻环状伪影,300dpi 打印放大后边缘发虚、不如 PNG 锐利。 + * 现回归 PNG(无损):以体积换清晰度,导出/打印文字边缘真正锐利。 + * 体积敏感场景可传 `imageType: 'jpeg'`,但默认走 PNG 保清晰。 + * + * ## 图表走矢量(svg2pdf) + * 设计器一切以 SVG 为真相源。为让图表在 PDF 里达到「印刷级矢量」: + * 1. 每页先**剥离 chart 控件** → 栅格化(文本/表格位图底图)→ addImage 作背景; + * 2. 再遍历该页 chart 节点,把 SVG 里的 `` 用 opentype.js 展开为**矢量字形轮廓**, + * 再用 `svg2pdf` 把其 SVG 以**矢量**注入 jsPDF 对应 mm 盒; + * (关键:jsPDF 只能内嵌 TrueType 字体、无法内嵌 CFF/OpenType,而思源宋体恰为 CFF, + * 直接注册会静默失败导致中文标签空白——转轮廓后文字与字体完全解耦,中文必现。) + * + * 兜底: + * - 图表被旋转 → 整页退回旧的全栅格化(旋转在矢量叠加里不易对齐); + * - 单个图表矢量失败/字形展开失败 → 该图表单独栅格化后叠加(图表保持 PNG 以保边缘锐利); + * - 整页矢量叠加彻底失败 → 整页退回全栅格化(仍是位图,但保证有图)。 + * + * 非图表页(无 chart 控件)仍走原全栅格化位图路径,行为与旧版一致。 + */ +import type { LayoutResult, LayoutPage, PlacedNode, PlacedControl } from '@/core/layout-engine/types' +import { pageToImageBlob, buildPageSvg, svgToCanvas } from './rasterize' +import { embedFontsInSvg, type FontFaceDef } from './fonts' +import type { PageDecoration } from '@/types/template' +import { blobToDataURL } from './util' +import { mmv } from '@/core/renderer-html/css-generator' +import { outlineChartSvgText } from './chart-svg-to-path' + +/** 思源宋体 —— 位图底图(含非图表文本)内联字体,保证中文衬线效果 */ +const PDF_FONTS: FontFaceDef[] = [ + { family: '思源宋体', src: '/fonts/SourceHanSerifCN-Regular.ttf', weight: 400 }, +] + +export interface PdfOptions { + /** 高清倍率,1=96dpi / 2=192dpi / 3=288dpi(默认·最高清),可传 4 更锐 */ + scale?: number + /** 页面装饰(背景色 + 水印),使 PDF 与预览一致 */ + pageDecoration?: PageDecoration + /** + * 位图底图的压缩格式(默认 `'png'`,无损最清晰): + * - `'png'`:无损,文字/线条边缘真正锐利,打印推荐;体积较大(纯文字页 ~10+ MB)。 + * - `'jpeg'`:体积小(~1-2 MB),仅体积敏感场景使用,清晰度略逊于 PNG。 + * 图表矢量叠加路径不受影响(图表本身走 svg2pdf 矢量注入)。 + */ + imageType?: 'jpeg' | 'png' +} + +/** 把 PdfOptions 的 imageType 解析成 canvas.toBlob 的 MIME 与 addImage 的格式名 */ +function imgSpec(imageType: 'jpeg' | 'png'): { mime: 'image/jpeg' | 'image/png'; addImageFmt: 'JPEG' | 'PNG' } { + return imageType === 'png' + ? { mime: 'image/png', addImageFmt: 'PNG' } + : { mime: 'image/jpeg', addImageFmt: 'JPEG' } +} + +function isChartNode(n: PlacedNode): n is PlacedControl { + return n.kind === 'control' && (n as PlacedControl).control.type === 'chart' +} + +/** 返回去掉所有 chart 控件后的页面副本(不影响原对象) */ +function stripCharts(page: LayoutPage): LayoutPage { + const f = (nodes: T[]): T[] => nodes.filter((n) => !isChartNode(n)) as T[] + return { ...page, header: f(page.header), body: f(page.body), footer: f(page.footer) } +} + +interface ChartBox { + left: number + top: number + width: number + height: number + angle: number + svg: string +} + +/** 收集页面内所有 chart 控件(含其 mm 盒与 SVG) */ +function chartsOf(page: LayoutPage): ChartBox[] { + const out: ChartBox[] = [] + for (const sec of [page.header, page.body, page.footer]) { + for (const n of sec) { + if (!isChartNode(n)) continue + const c = n as PlacedControl + if (c.content.kind !== 'svg') continue + out.push({ + left: n.left, + top: n.top, + width: n.width, + height: n.height, + angle: n.angle ?? 0, + svg: c.content.svg, + }) + } + } + return out +} + +/** 单图表栅格化(svg2pdf 失败时的兜底,保证不丢图) */ +async function rasterizeChart(chartSvg: string, wMm: number, hMm: number, scale: number): Promise { + const inner = `
${chartSvg}
` + let svg = buildPageSvg(inner, '', wMm, hMm) + svg = await embedFontsInSvg(svg, PDF_FONTS) + const canvas = await svgToCanvas(svg, scale, '#ffffff') + const blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b), 'image/png')) + if (!blob) throw new Error('图表栅格化失败') + return blobToDataURL(blob) +} + +/** SVG 字符串 → 挂到 DOM 的 SVGSVGElement(svg2pdf 需要真实节点以读取几何) */ +function svgElementFromString(str: string): SVGSVGElement { + const doc = new DOMParser().parseFromString(str, 'image/svg+xml') + return doc.documentElement as unknown as SVGSVGElement +} + +export async function documentToPdf(result: LayoutResult, opts: PdfOptions = {}): Promise { + const mod = await import('jspdf') + const JsPDF = (mod as unknown as { jsPDF?: typeof mod.default; default: typeof mod.default }).jsPDF ?? mod.default + const { pageWidth: w, pageHeight: h } = result.metrics + const orientation = w > h ? 'landscape' : 'portrait' + const scale = opts.scale ?? 3 + const imageType: 'jpeg' | 'png' = opts.imageType ?? 'png' + const { addImageFmt } = imgSpec(imageType) + + const doc = new JsPDF({ unit: 'mm', format: [w, h], orientation }) + + for (let i = 0; i < result.pages.length; i++) { + if (i > 0) doc.addPage([w, h], orientation) + + const page = result.pages[i]! + const charts = chartsOf(page) + // 有图表且该页图表都未旋转 → 走矢量;否则全栅格化(含图表) + const useVector = charts.length > 0 && charts.every((c) => c.angle === 0) + + if (!useVector) { + const dataUrl = await rasterizePageFull(result, i, scale, opts.pageDecoration, imageType) + doc.addImage(dataUrl, addImageFmt, 0, 0, w, h) + continue + } + + // 矢量路径:先栅格化"剥掉图表"的底图,再把图表以矢量叠加(文字已转轮廓,无需内嵌字体) + const strippedResult: LayoutResult = { + ...result, + pages: result.pages.map((p, idx) => (idx === i ? stripCharts(p) : p)), + } + const baseUrl = await rasterizePageFull(strippedResult, i, scale, opts.pageDecoration, imageType) + doc.addImage(baseUrl, addImageFmt, 0, 0, w, h) + + let allVectorOk = true + for (const ch of charts) { + try { + // 字形转轮廓:把 展开为矢量 path,避免 jsPDF 无法内嵌 CFF 字体导致中文空白 + const outlinedSvg = await outlineChartSvgText(ch.svg) + const el = svgElementFromString(outlinedSvg) + el.style.position = 'absolute' + el.style.left = '-99999px' + el.style.top = '0' + document.body.appendChild(el) + try { + const { svg2pdf } = await import('svg2pdf.js') + await svg2pdf(el, doc, { x: ch.left, y: ch.top, width: ch.width, height: ch.height }) + } finally { + document.body.removeChild(el) + } + } catch { + // 该图表矢量失败 → 单独栅格化兜底(图表保持 PNG 以保边缘锐利,体积可控) + try { + const img = await rasterizeChart(ch.svg, ch.width, ch.height, scale) + doc.addImage(img, 'PNG', ch.left, ch.top, ch.width, ch.height) + } catch { + allVectorOk = false + } + } + } + + // 兜底:若整页矢量叠加彻底失败,重渲染该页(含图表)作位图背景 + if (!allVectorOk) { + // 当前页已 addImage 了"剥图底图",需替换为含图表的整页位图。 + // 顺序处理下最后一页动作即本页,删除后重建即可,不影响其它页。 + const pageCount = doc.getNumberOfPages() + doc.deletePage(pageCount) + doc.addPage([w, h], orientation) + const fallback = await rasterizePageFull(result, i, scale, opts.pageDecoration, imageType) + doc.addImage(fallback, addImageFmt, 0, 0, w, h) + } + } + + return doc.output('blob') +} + +/** 整页栅格化(含图表),封装原 pageToImageBlob 调用 */ +async function rasterizePageFull( + result: LayoutResult, + index: number, + scale: number, + pageDecoration: PageDecoration | undefined, + imageType: 'jpeg' | 'png', +): Promise { + const { mime } = imgSpec(imageType) + const blob = await pageToImageBlob(result, index, { + type: mime, + scale, + background: pageDecoration?.backgroundColor ?? '#ffffff', + fonts: PDF_FONTS, + pageDecoration, + }) + return blobToDataURL(blob) +} diff --git a/openprint/src/core/export-engine/export-svg.spec.ts b/openprint/src/core/export-engine/export-svg.spec.ts new file mode 100644 index 0000000..e6b0ad2 --- /dev/null +++ b/openprint/src/core/export-engine/export-svg.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { layout } from '@/core/layout-engine/pagination-engine' +import { documentToSvgString } from '@/core/export-engine/export-svg' +import { mmToPx } from '@/core/units' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + const price = 10 + (i % 9) + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price, + amount: qty * price, + } + }) + return { order: { orderNo: 'SO-001' }, customer: { name: '客户' }, items } +} + +describe('documentToSvgString —— 多页矢量 SVG', () => { + it('产出多页堆叠合法 SVG(foreignObject + 中文 + 精确像素)', async () => { + const template = createDemoTemplate() + const result = await layout(template, makeData(30), { measurer: createCjkMeasurer() }) + const svg = await documentToSvgString(result) + + expect(svg).toContain(' + expect((svg.match(/ + * + * 产物是单文件、多页纵向堆叠的 SVG(每页一个 ``), + * 浏览器 / Inkscape / 矢量软件都能打开,且因为是同一份 HTML+CSS, + * 与预览、PDF、打印视觉完全一致。 + */ +import { mmToPx } from '@/core/units' +import { renderPage, generateCss } from '@/core/renderer-html' +import type { LayoutResult } from '@/core/layout-engine/types' +import type { PageDecoration } from '@/types/template' +import { toXmlSafe } from './rasterize' +import { embedFontsInSvg, type FontFaceDef } from './fonts' + +export async function documentToSvgString( + result: LayoutResult, + fonts?: FontFaceDef[], + pageDecoration?: PageDecoration, +): Promise { + const { pageWidth, pageHeight } = result.metrics + const w = Math.max(1, Math.ceil(mmToPx(pageWidth))) + const h = Math.max(1, Math.ceil(mmToPx(pageHeight))) + const css = generateCss(result.metrics, { screen: false, pageDecoration }) + + const pages = result.pages + .map((page, i) => { + const safe = toXmlSafe(renderPage(page, pageDecoration?.watermark)) + const inner = + `
` + + `` + + `
${safe}
` + + `
` + return ( + `` + + `${inner}` + + `` + ) + }) + .join('\n') + + const totalH = h * result.pages.length + let svg = ( + `` + + `` + + pages + + `` + ) + if (fonts?.length) svg = await embedFontsInSvg(svg, fonts) + return svg +} diff --git a/openprint/src/core/export-engine/fonts.ts b/openprint/src/core/export-engine/fonts.ts new file mode 100644 index 0000000..17e135d --- /dev/null +++ b/openprint/src/core/export-engine/fonts.ts @@ -0,0 +1,103 @@ +/** + * 字体嵌入工具 —— 让栅格化产物(PNG / JPG / PDF)与矢量 SVG / 无头打印 HTML 也能用上「自定义字体」。 + * + * ## 为什么需要它 + * - 预览 / 浏览器打印:字体由主文档或 iframe 的 `@font-face` 提供,正常生效。 + * - 但 PNG/JPG/PDF 走 `SVG ` 光栅化,而 `` 加载的 SVG 是**隔离上下文**, + * 看不到父文档的 `@font-face`,只能用系统字体(PingFang SC / 系统 sans-serif 兜底)。 + * 若模板指定了「思源宋体」等自定义字体,栅格化产物会丢字体。 + * - 解法:把字体以 `@font-face(data-URI)` 直接写进 SVG / HTML 本身,隔离上下文也能用。 + * + * 仅当 `fonts` 显式传入才做网络读取(同源本地字体),默认不联网,符合零网络铁律。 + */ +export interface FontFaceDef { + /** 字体族名,必须与模板 CSS 中的 font-family 一致 */ + family: string + /** 同源字体文件 URL(.woff2 / .ttf / .otf / .woff) */ + src: string + weight?: string | number + style?: string +} + +function fontFormat(src: string): string { + const ext = (src.split('?')[0] ?? '').split('.').pop()?.toLowerCase() + if (ext === 'ttf') return 'truetype' + if (ext === 'otf') return 'opentype' + if (ext === 'woff2') return 'woff2' + if (ext === 'eot') return 'embedded-opentype' + return 'woff' +} + +function fontMime(src: string): string { + const ext = (src.split('?')[0] ?? '').split('.').pop()?.toLowerCase() + if (ext === 'ttf') return 'font/ttf' + if (ext === 'otf') return 'font/otf' + if (ext === 'eot') return 'application/vnd.ms-fontobject' + if (ext === 'woff') return 'font/woff' + return 'font/woff2' +} + +async function fetchFontDataUri(src: string): Promise { + const res = await fetch(src) + if (!res.ok) throw new Error(`字体加载失败 ${src}: ${res.status}`) + const buf = await res.arrayBuffer() + const bytes = new Uint8Array(buf) + let bin = '' + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return `data:${fontMime(src)};base64,${btoa(bin)}` +} + +function buildFontFaceCss(defs: FontFaceDef[], dataUris: string[]): string { + return defs + .map( + (d, i) => + `@font-face{font-family:"${d.family}";src:url(${dataUris[i]}) format("${fontFormat( + d.src, + )}");font-weight:${d.weight ?? 'normal'};font-style:${d.style ?? 'normal'};}`, + ) + .join('') +} + +const XHTML_DIV = '
' + +/** 把字体以 data-URI @font-face 嵌进 SVG(每个 xhtml div 顶部)—— 供 rasterize / SVG 矢量导出使用 + * 单个字体加载失败(文件缺失 / 网络异常)时跳过该字体,走系统字体兜底,不阻塞导出。 */ +export async function embedFontsInSvg(svg: string, defs: FontFaceDef[]): Promise { + if (!defs.length) return svg + const pairs = await Promise.all( + defs.map(async (d) => { + try { + return { d, uri: await fetchFontDataUri(d.src) } + } catch { + return null + } + }), + ) + const ok = pairs.filter((p): p is { d: FontFaceDef; uri: string } => p !== null) + if (!ok.length) return svg + const css = `` + return svg.split(XHTML_DIV).join(`${XHTML_DIV}${css}`) +} + +/** 把字体以 data-URI @font-face 嵌进 HTML —— 供无头静默打印文档使用 + * 单个字体加载失败(文件缺失 / 网络异常)时跳过该字体,走系统字体兜底,不阻塞打印。 */ +export async function embedFontsInHtml(html: string, defs: FontFaceDef[]): Promise { + if (!defs.length) return html + const pairs = await Promise.all( + defs.map(async (d) => { + try { + return { d, uri: await fetchFontDataUri(d.src) } + } catch { + return null + } + }), + ) + const ok = pairs.filter((p): p is { d: FontFaceDef; uri: string } => p !== null) + if (!ok.length) return html + const style = `` + if (html.includes('')) return html.replace('', `${style}`) + return `${style}${html}` +} diff --git a/openprint/src/core/export-engine/index.ts b/openprint/src/core/export-engine/index.ts new file mode 100644 index 0000000..6a49705 --- /dev/null +++ b/openprint/src/core/export-engine/index.ts @@ -0,0 +1,98 @@ +/** + * Export Engine —— 三格式导出(PDF / JPG / SVG) + * + * 纯 TypeScript、零框架依赖(与 designer-vue 解耦,headless 阶段可直接复用)。 + * 真相来源统一是 `sdk.render()` 的 HTML 产物,因此: + * - 预览 = 打印 = 导出 三者视觉一致 + * - 支持数据绑定 + 超长表格分页 + 中文(系统字体) + * + * 导出语义: + * - PDF:单文件、多页(位图 PDF,思源宋体内联) + * - JPG:每页一个文件(多页时按 `name-1.jpg` 命名) + * - SVG:单文件、多页纵向堆叠(矢量,含 foreignObject) + */ +import type { RenderRequest } from '@/core/sdk' +import { render } from '@/core/sdk' +import type { LayoutResult } from '@/core/layout-engine/types' +import { documentToPdf } from './export-pdf' +import { documentToSvgString } from './export-svg' +import { pageToJpg } from './export-image' +import { downloadBlob } from './util' +import type { FontFaceDef } from './fonts' + +export type { FontFaceDef } from './fonts' + +export type ExportFormat = 'pdf' | 'jpg' | 'svg' + +export interface ExportOptions { + /** 高清倍率 1/2/3(默认 3·288dpi·最高清),可传 4 更锐 */ + scale?: number + /** 文件名(不含扩展名);多页 JPG 会追加 -页码 */ + filename?: string + /** 位图背景色,默认白 */ + background?: string + /** 自定义字体(同源 URL),嵌入 JPG/SVG 栅格化产物使导出不丢字体 */ + fonts?: FontFaceDef[] + /** + * PDF 位图底图压缩格式(仅 PDF 导出生效,默认 `'png'` 无损最清晰): + * - `'png'`:无损,文字/线条边缘锐利,打印推荐; + * - `'jpeg'`:体积小,仅体积敏感场景。 + */ + pdfImageType?: 'jpeg' | 'png' +} + +export interface ExportOutcome { + blobs: Blob[] + filenames: string[] + mime: string +} + +/** + * 渲染 + 导出一站式入口。PDF/SVG 单文件多页,JPG 每页一个文件。 + * 调用方逐一下载即可。 + */ +export async function exportDocument( + request: RenderRequest, + format: ExportFormat, + options: ExportOptions = {}, +): Promise { + const res = await render(request) + const result: LayoutResult = res.result + const name = (options.filename ?? 'openprint-document').trim() || 'openprint-document' + const scale = options.scale ?? 3 + const bg = options.background ?? '#ffffff' + // 页面装饰(背景色 + 水印)统一来自渲染请求 output,三格式导出共享 + const deco = request.output?.pageDecoration + + if (format === 'pdf') { + const blob = await documentToPdf(result, { + scale, + imageType: options.pdfImageType ?? 'png', + pageDecoration: deco, + }) + return { blobs: [blob], filenames: [`${name}.pdf`], mime: 'application/pdf' } + } + + if (format === 'svg') { + const svg = await documentToSvgString(result, options.fonts, deco) + const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }) + return { blobs: [blob], filenames: [`${name}.svg`], mime: 'image/svg+xml' } + } + + // JPG:每页一个文件 + const blobs = await Promise.all( + result.pages.map((_, i) => + pageToJpg(result, i, { scale, background: bg, fonts: options.fonts, pageDecoration: deco }), + ), + ) + const filenames = blobs.map((_, i) => + blobs.length > 1 ? `${name}-${i + 1}.jpg` : `${name}.jpg`, + ) + return { blobs, filenames, mime: 'image/jpeg' } +} + +export { downloadBlob, pageToJpg, documentToSvgString, documentToPdf } +export type { RenderRequest } from '@/core/sdk' +export type { LayoutResult } from '@/core/layout-engine/types' +export type { PdfOptions as ExportPdfOptions } from './export-pdf' +export type { PageImageOptions } from './rasterize' diff --git a/openprint/src/core/export-engine/rasterize.spec.ts b/openprint/src/core/export-engine/rasterize.spec.ts new file mode 100644 index 0000000..44bd4f0 --- /dev/null +++ b/openprint/src/core/export-engine/rasterize.spec.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { inlinePageImages, toXmlSafe } from '@/core/export-engine/rasterize' + +describe('toXmlSafe —— HTML→XML 安全(foreignObject 光栅化前置)', () => { + it('void 标签补自闭合', () => { + expect(toXmlSafe('')).toBe('') + expect(toXmlSafe('
')).toBe('
') + expect(toXmlSafe('
')).toBe('') + expect(toXmlSafe('
')).toBe('
') + expect(toXmlSafe('')).toBe('') + }) + + it('已自闭合的 void 标签保持单一斜杠', () => { + // 带属性的自闭合:补成标准 " />" 形式 + expect(toXmlSafe('')).toBe('') + // 无属性的自闭合(如
)正则不触碰,原样保留也是正确的 XML + expect(toXmlSafe('
')).toBe('
') + }) + + it('内嵌 缺命名空间时补上', () => { + expect(toXmlSafe('')).toBe('') + expect(toXmlSafe('')).toBe( + '', + ) + }) + + it('已带命名空间的 不再重复补', () => { + const src = '' + expect(toXmlSafe(src)).toBe(src) + }) + + it('普通标签与文本不受影响', () => { + expect(toXmlSafe('
你好世界
')).toBe( + '
你好世界
', + ) + }) +}) + +describe('inlinePageImages —— 外部图片内联(防 canvas taint)', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]) + const stubFetch = (mime = 'image/png') => + vi.stubGlobal( + 'fetch', + vi.fn(async () => ({ + ok: true, + headers: { get: () => mime }, + arrayBuffer: async () => png.buffer, + })), + ) + + it('无图片时原样返回', async () => { + const html = '
hello
' + expect(await inlinePageImages(html)).toBe(html) + }) + + it('外部图片 src 被替换为 data URI(btoa 可用环境)', async () => { + stubFetch() + const out = await inlinePageImages('') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('单引号 src 的 同样被内联(此前漏网导致 canvas taint)', async () => { + stubFetch() + const out = await inlinePageImages("") + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('CSS url(...) 背景图被内联', async () => { + stubFetch() + const out = await inlinePageImages('
') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('SVG 被内联', async () => { + stubFetch() + const out = await inlinePageImages('') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('加载失败的图片降级为透明占位,不阻塞导出', async () => { + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 404 }))) + const out = await inlinePageImages('') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('/missing.png') + }) + + it('data URI 图片保持原样(不重复内联)', async () => { + const html = '' + expect(await inlinePageImages(html)).toBe(html) + }) + + it('data URI 的 CSS url 保持原样', async () => { + const html = '
' + expect(await inlinePageImages(html)).toBe(html) + }) +}) diff --git a/openprint/src/core/export-engine/rasterize.ts b/openprint/src/core/export-engine/rasterize.ts new file mode 100644 index 0000000..b920ca3 --- /dev/null +++ b/openprint/src/core/export-engine/rasterize.ts @@ -0,0 +1,280 @@ +/** + * 栅格化工具 —— 把单页渲染 HTML 变成位图 + * + * ## 为什么用 SVG + * + * 设计器/渲染引擎的"真相来源"是 `render()` 产出的 HTML/CSS(§2 单一真相源)。 + * 要把 HTML 变成 PNG/JPG,浏览器原生只有两条路: + * 1. html2canvas 之类的库(要新依赖,且零网络铁律下不想引) + * 2. 把 HTML 塞进 SVG 的 ``,再交给 ``/`canvas` 光栅化 + * + * 这里走第 2 条:纯浏览器 API、零依赖、中文靠系统字体(PingFang SC / 思源)渲染。 + * + * ## 已知边界 + * - `` 在 Chromium / Firefox 用 `` 光栅化是支持的; + * Safari 出于安全限制**不支持** img 里的 foreignObject,需要换 html2canvas 才能覆盖。 + * 当前目标浏览器为 Chromium 内核,足够。 + * - 内嵌资源必须同源或 data URI(本设计器图片已走 data URI,条码/二维码是内联 SVG)。 + */ +import { mmToPx } from '@/core/units' +import { renderPage, generateCss } from '@/core/renderer-html' +import type { LayoutResult } from '@/core/layout-engine/types' +import type { PageDecoration } from '@/types/template' +import { embedFontsInSvg, type FontFaceDef } from './fonts' + +/** HTML 转 XML 安全的 void 标签集合(foreignObject 走 XML 解析,未闭合会整页渲染失败) */ +const VOID_TAGS = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', + 'meta', 'param', 'source', 'track', 'wbr', +]) + +/** + * 把渲染产物里可能出现的不规范 HTML 修成 XML 安全: + * - void 标签补自闭合 `/>` + * - 内嵌 `` 确保带 SVG 命名空间(否则 XHTML 命名空间下不渲染) + */ +export function toXmlSafe(html: string): string { + const fixed = html.replace( + /<([a-zA-Z][\w-]*)((?:\s+[^<>]*?)?)\s*>/g, + (m, tag: string, attrs: string) => { + const t = tag.toLowerCase() + if (VOID_TAGS.has(t)) { + const clean = attrs.replace(/\/\s*$/, '') + return `<${t}${clean} />` + } + return m + }, + ) + // 给缺命名空间的内嵌 svg 补上(负向预查:后面到 > 之间不含 xmlns= 才补) + return fixed.replace(/]*\bxmlns=)/g, '` + + `` + + `
${safe}
` + + `` + return ( + `` + + `${inner}` + + `` + ) +} + +/** SVG 光栅化超时(ms):部分环境对 支持差 / onload 永不触发, + * 超时则降级返回白底空 canvas,不让单页失败拖垮整份导出。 */ +const SVG_RASTER_TIMEOUT = 4000 + +function loadImage(src: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + let done = false + const timer = setTimeout(() => { + if (done) return + done = true + reject(new Error('SVG 光栅化超时(环境不支持 foreignObject 或资源跨域)')) + }, SVG_RASTER_TIMEOUT) + img.onload = () => { + if (done) return + done = true + clearTimeout(timer) + resolve(img) + } + img.onerror = () => { + if (done) return + done = true + clearTimeout(timer) + reject(new Error('SVG 光栅化失败(浏览器不支持 foreignObject 或资源跨域)')) + } + img.src = src + }) +} + +/** SVG 字符串 → 高清 canvas(scale 倍图,填白底避免透明)。超时/失败时降级白底空 canvas + * + * ## 为什么用 data: URL 而不是 blob: URL(血泪教训,勿改回) + * Chromium 安全策略:经 `blob:` URL 加载的、含 `` 的 SVG 会被判定为 + * 「来源不纯净」,drawImage 后 canvas 被污染,toBlob/toDataURL 抛 + * "Tainted canvases may not be exported"。同一 SVG 改用 `data:` URL 加载则不污染。 + * (实测对照:blob+FO=TAINTED,blob+纯矢量=clean,data+FO=clean,data+纯矢量=clean) + */ +/** 把 SVG 根节点的显示宽高放大 scale 倍(viewBox 不变),让 foreignObject 以高分原生渲染, + * 而不是低分渲染后再插值放大 —— 这是打印/导出文字清晰的关键。 + * 血泪:低分 SVG 渲染成位图后再 drawImage 放大 = 只是像素插值,文字发虚; + * 放大 SVG 的 width/height(viewBox 不动)则内容矢量重排到高分画布,边缘锐利。 */ +function scaleSvgViewport(svg: string, scale: number): string { + if (!Number.isFinite(scale) || scale === 1) return svg + return svg.replace( + /]*?)\bwidth="([\d.]+)"([^>]*?)\bheight="([\d.]+)"/, + (m, p1: string, w: string, p2: string, h: string) => + ` { + // 先放大 SVG 显示尺寸,再加载:img 本身就是高分位图,canvas 无需再插值放大 + const scaled = scaleSvgViewport(svg, scale) + const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(scaled) + // fallback 尺寸从放大后的 SVG 宽高推导(取不到再用 A4×scale 兜底) + const dim = scaled.match(/]*\bwidth="([\d.]+)"[^>]*\bheight="([\d.]+)"/) + const cw = Math.max(1, Math.round(dim ? parseFloat(dim[1]!) : 794 * scale)) + const ch = Math.max(1, Math.round(dim ? parseFloat(dim[2]!) : 1123 * scale)) + const fallback = (): HTMLCanvasElement => { + const canvas = document.createElement('canvas') + canvas.width = cw + canvas.height = ch + const ctx = canvas.getContext('2d') + if (ctx) { + ctx.fillStyle = bg + ctx.fillRect(0, 0, cw, ch) + } + return canvas + } + try { + const img = await loadImage(url) + const canvas = document.createElement('canvas') + // img.width/height 已是高分目标尺寸,drawImage 原样铺绘,不再缩放插值 + canvas.width = Math.max(1, img.width) + canvas.height = Math.max(1, img.height) + const ctx = canvas.getContext('2d') + if (!ctx) return fallback() + ctx.fillStyle = bg + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.drawImage(img, 0, 0) + return canvas + } catch { + // 环境不支持 foreignObject 光栅化(如 happy-dom)→ 降级白底空 canvas,不阻塞导出 + return fallback() + } +} + +export interface PageImageOptions { + scale?: number + background?: string + type?: 'image/png' | 'image/jpeg' + /** + * JPEG 编码质量 0-1,默认 **1(最高清)**。 + * 注:quality=1 比 0.92 体积大约 30-60%,但仍是 PNG 的零头; + * 对票据/报表这类对清晰度敏感的打印件值得。 + */ + quality?: number + /** 自定义字体(同源 URL),嵌入 SVG 使栅格化产物不丢字体 */ + fonts?: FontFaceDef[] + /** 页面装饰(背景色 + 水印),使导出与预览一致 */ + pageDecoration?: PageDecoration +} + +/* ------------------------- 图片内联(防 canvas taint) ------------------------- */ + +/** 1×1 透明 PNG,外部图片加载失败时的降级占位(避免整页导出失败) */ +const TRANSPARENT_PNG = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + +/** 的 src(双引号或单引号) */ +const IMG_SRC_RE = /]*?\bsrc=(["'])([^"']*)\1/gi +/** CSS url(...) 引用(background / @font-face 等,仅图片类会被内联) */ +const CSS_URL_RE = /url\(\s*(["']?)([^"')]+)\1\s*\)/gi +/** SVG 的 href / xlink:href */ +const SVG_IMAGE_RE = /]*?\b(?:xlink:)?href=(["'])([^"']*)\1/gi + +async function fetchAsDataUri(src: string): Promise<{ uri: string; mime: string }> { + const res = await fetch(src) + if (!res.ok) throw new Error(`资源加载失败 ${src}: ${res.status}`) + const bytes = new Uint8Array(await res.arrayBuffer()) + const mime = res.headers.get('content-type')?.split(';')[0]?.trim() || 'image/png' + let bin = '' + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return { uri: `data:${mime};base64,${btoa(bin)}`, mime } +} + +/** 收集页面 HTML 里所有外部资源引用(img src / CSS url / SVG image href) */ +function collectExternalSrcs(html: string): Set { + const srcs = new Set() + const add = (src: string | undefined) => { + const s = (src ?? '').trim() + if (!s || /^data:/i.test(s) || /^#/.test(s) || /^blob:/i.test(s)) return + srcs.add(s) + } + for (const m of html.matchAll(IMG_SRC_RE)) add(m[2]) + for (const m of html.matchAll(CSS_URL_RE)) add(m[2]) + for (const m of html.matchAll(SVG_IMAGE_RE)) add(m[2]) + return srcs +} + +/** + * 把页面 HTML 里所有外部图片(http / 相对路径)内联成 data URI。 + * 否则 SVG 加载跨域图片会污染 canvas,toBlob 抛 + * "Tainted canvases may not be exported"。失败/跨域图片降级为透明占位,不阻塞导出。 + */ +export async function inlinePageImages(html: string): Promise { + const srcs = collectExternalSrcs(html) + if (!srcs.size) return html + + const resolved = new Map() + await Promise.all( + [...srcs].map(async (src) => { + try { + const { uri } = await fetchAsDataUri(src) + resolved.set(src, uri) + } catch { + resolved.set(src, TRANSPARENT_PNG) + } + }), + ) + + // 替换 (双/单引号) + let out = html.replace(IMG_SRC_RE, (tag, quote: string, src: string) => { + const uri = resolved.get(src.trim()) + return uri ? tag.replace(`src=${quote}${src}${quote}`, `src=${quote}${uri}${quote}`) : tag + }) + // 替换 CSS url(...) + out = out.replace(CSS_URL_RE, (tag, quote: string, src: string) => { + const uri = resolved.get(src.trim()) + return uri ? `url(${quote}${uri}${quote})` : tag + }) + // 替换 SVG + out = out.replace(SVG_IMAGE_RE, (tag, quote: string, src: string) => { + const uri = resolved.get(src.trim()) + return uri ? tag.replace(/href=(["'])[^"']*\1/i, `href=${quote}${uri}${quote}`) : tag + }) + return out +} + +/** 把某一页渲染成位图 Blob(PNG / JPG 共用) */ +export async function pageToImageBlob( + result: LayoutResult, + index: number, + opts: PageImageOptions = {}, +): Promise { + const page = result.pages[index] + if (!page) throw new Error(`页码越界:${index}`) + const deco = opts.pageDecoration + const css = generateCss(result.metrics, { screen: false, pageDecoration: deco }) + // 先内联图片为 data URI,再进 SVG 栅格化 —— 防止外部图污染 canvas 导致 toBlob 失败 + const pageHtml = await inlinePageImages(renderPage(page, deco?.watermark)) + let svg = buildPageSvg(pageHtml, css, result.metrics.pageWidth, result.metrics.pageHeight) + if (opts.fonts?.length) svg = await embedFontsInSvg(svg, opts.fonts) + const canvas = await svgToCanvas(svg, opts.scale ?? 3, opts.background ?? '#ffffff') + const type = opts.type ?? 'image/png' + const quality = type === 'image/jpeg' ? opts.quality ?? 1 : undefined + return new Promise((resolve, reject) => { + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error('canvas.toBlob 失败'))), + type, + quality, + ) + }) +} diff --git a/openprint/src/core/export-engine/util.ts b/openprint/src/core/export-engine/util.ts new file mode 100644 index 0000000..7c8b774 --- /dev/null +++ b/openprint/src/core/export-engine/util.ts @@ -0,0 +1,25 @@ +/** + * 浏览器侧小工具:Blob → dataURL、触发下载 + */ + +export function blobToDataURL(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(new Error('FileReader 读取失败')) + reader.readAsDataURL(blob) + }) +} + +/** 触发浏览器下载(单文件) */ +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + // 延迟回收,确保下载已触发 + setTimeout(() => URL.revokeObjectURL(url), 1500) +} diff --git a/openprint/src/core/fonts/catalog.ts b/openprint/src/core/fonts/catalog.ts new file mode 100644 index 0000000..b2e1a7b --- /dev/null +++ b/openprint/src/core/fonts/catalog.ts @@ -0,0 +1,79 @@ +/** + * 字体注册中心(《设计方案》§11.6 @open-print/fonts registry) + * + * 内置字体清单:font-family 名 → 字体文件(public/fonts/ 同源静态资源,零网络铁律)。 + * 命名与 `参考资料/fonts/fonts.txt` 一致: + * - HedvigLettersSans-Regular.woff2 → Hedvig Letters Sans + * - LiberationSerif-{Regular,Bold,Italic}.woff2 → Liberation Serif(一个族,三种字重/字形) + * - SourceHanSansCN-Normal.woff2 → 思源黑体 + * - SourceHanSerifCN-Regular.ttf → 思源宋体 + * - ChillKai.woff2 → 寒蝉正楷体 + */ + +export interface FontFaceEntry { + /** 同源字体文件 URL(相对 public 根;浏览器/位图栅格化用,woff2 优先) */ + src: string + weight?: number + style?: 'normal' | 'italic' +} + +export interface FontFamilyDef { + /** 字体族名(模板 style.fontFamily / CSS font-family 用这个名字) */ + family: string + /** 展示名(属性面板下拉用) */ + label: string + /** 优先级(属性面板排序) */ + order: number + faces: FontFaceEntry[] +} + +/** 内置字体清单(顺序即面板展示顺序;woff2 供浏览器显示与位图 PDF 栅格化内联) */ +export const FONT_CATALOG: FontFamilyDef[] = [ + { + family: '思源黑体', + label: '思源黑体', + order: 1, + faces: [{ src: '/fonts/SourceHanSansCN-Normal.woff2', weight: 400 }], + }, + { + family: '思源宋体', + label: '思源宋体', + order: 2, + faces: [{ src: '/fonts/SourceHanSerifCN-Regular.ttf', weight: 400 }], + }, + { + family: '寒蝉正楷体', + label: '寒蝉正楷体', + order: 3, + faces: [{ src: '/fonts/ChillKai.woff2', weight: 400 }], + }, + { + family: 'Liberation Serif', + label: 'Liberation Serif', + order: 4, + faces: [ + { src: '/fonts/LiberationSerif-Regular.woff2', weight: 400 }, + { src: '/fonts/LiberationSerif-Bold.woff2', weight: 700 }, + { src: '/fonts/LiberationSerif-Italic.woff2', weight: 400, style: 'italic' }, + ], + }, + { + family: 'Hedvig Letters Sans', + label: 'Hedvig Letters Sans', + order: 5, + faces: [{ src: '/fonts/HedvigLettersSans-Regular.woff2', weight: 400 }], + }, +] + +/** 按字体族名查找(模板 style.fontFamily 可能存家族名,如 "思源黑体") */ +export function findFontFamily(family: string | undefined): FontFamilyDef | undefined { + if (!family) return undefined + const name = (family.replace(/^"|"$/g, '').split(',')[0] ?? '').trim() + return FONT_CATALOG.find((f) => f.family === name) +} + +/** 字体文件相对 public 根 → 绝对 URL(预览 iframe 注入用;浏览器环境) */ +export function fontUrl(src: string): string { + if (typeof window === 'undefined') return src + return new URL(src, window.location.origin).href +} diff --git a/openprint/src/core/fonts/fonts.spec.ts b/openprint/src/core/fonts/fonts.spec.ts new file mode 100644 index 0000000..9859286 --- /dev/null +++ b/openprint/src/core/fonts/fonts.spec.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { FONT_CATALOG, findFontFamily } from '@/core/fonts/catalog' +import { builtinFontFaceCss, templateUsedFonts } from '@/core/fonts/loader' +import type { TemplateData } from '@/types/template' +import type { AnyControl } from '@/types/control' + +describe('fonts —— 内置字体包', () => { + it('目录覆盖参考资料 fonts.txt 全部字体族', () => { + const families = FONT_CATALOG.map((f) => f.family) + // fonts.txt 定义的名称(中文名/拉丁名) + expect(families).toContain('思源黑体') + expect(families).toContain('思源宋体') + expect(families).toContain('寒蝉正楷体') + expect(families).toContain('Liberation Serif') + expect(families).toContain('Hedvig Letters Sans') + // Liberation Serif 一个族包含 3 个字重/字形 + const lib = FONT_CATALOG.find((f) => f.family === 'Liberation Serif') + expect(lib?.faces.length).toBe(3) + }) + + it('builtinFontFaceCss 生成合法 @font-face 且含 format', () => { + const css = builtinFontFaceCss('http://localhost:5173') + expect(css).toContain('@font-face') + expect(css).toContain('font-family:"思源黑体"') + expect(css).toContain('format("woff2")') + expect(css).toContain('format("truetype")') // 思源宋体 ttf + expect(css).toContain('http://localhost:5173/fonts/SourceHanSansCN-Normal.woff2') + }) + + it('templateUsedFonts 只提取模板实际用到的字体族', () => { + const tpl: TemplateData = { + version: '1.0', + document: { + type: 'report', + page: { width: 210, height: 297, unit: 'mm', orientation: 'portrait', margin: { top: 10, right: 10, bottom: 10, left: 10 } }, + sections: [ + { + type: 'body', + components: [ + { id: 't1', type: 'text', left: 0, top: 0, width: 50, height: 8, value: 'x', style: { fontFamily: '思源黑体' }, printable: true }, + { id: 't2', type: 'text', left: 0, top: 10, width: 50, height: 8, value: 'y', printable: true }, + ], + }, + ], + }, + } + const used = templateUsedFonts(tpl) + expect(used.map((f) => f.family)).toEqual(['思源黑体']) + }) + + it('findFontFamily 容错带引号的族名', () => { + expect(findFontFamily('"思源宋体"')?.family).toBe('思源宋体') + expect(findFontFamily('思源黑体, sans-serif')?.family).toBe('思源黑体') + expect(findFontFamily(undefined)).toBeUndefined() + }) +}) diff --git a/openprint/src/core/fonts/loader.ts b/openprint/src/core/fonts/loader.ts new file mode 100644 index 0000000..1f8d5ea --- /dev/null +++ b/openprint/src/core/fonts/loader.ts @@ -0,0 +1,101 @@ +/** + * 字体本地加载(《设计方案》§11.6 @open-print/fonts loader) + * + * - `loadBuiltinFonts()`:用 FontFace API 把内置字体注册到浏览器(设计期画布/预览生效), + * 字体文件来自 public/fonts/ 同源静态资源,零网络依赖。 + * - `builtinFontFaceCss(baseUrl)`:生成 @font-face CSS(预览 iframe 注入用)。 + * - `templateUsedFonts(template)`:提取模板文本控件实际用到的字体族(供导出内联)。 + */ +import { FONT_CATALOG, findFontFamily, type FontFamilyDef } from './catalog' +import type { TemplateData } from '@/types/template' +import type { AnyControl, TextControl } from '@/types/control' +import type { FontFaceDef } from '@/core/export-engine/fonts' + +/** 浏览器环境是否可用 FontFace API */ +function canUseFontFace(): boolean { + return typeof window !== 'undefined' && typeof (window as unknown as { FontFace?: unknown }).FontFace !== 'undefined' +} + +/** 把内置字体注册到 document.fonts(浏览器);resolve 后返回已加载的族名列表 */ +export async function loadBuiltinFonts(defs: FontFamilyDef[] = FONT_CATALOG): Promise { + if (!canUseFontFace() || typeof document === 'undefined' || typeof window === 'undefined') return [] + const FontFaceCtor = ( + window as unknown as { + FontFace?: new (family: string, source: string | ArrayBuffer, descriptors?: FontFaceDescriptors) => FontFace + } + ).FontFace + if (!FontFaceCtor) return [] + const loaded: string[] = [] + const tasks: Promise[] = [] + for (const def of defs) { + for (const face of def.faces) { + try { + const fontFace = new FontFaceCtor( + def.family, + `url(${new URL(face.src, window.location.origin).href})`, + { + weight: String(face.weight ?? 'normal'), + style: face.style ?? 'normal', + }, + ) + tasks.push( + fontFace.load().then(() => { + document.fonts.add(fontFace) + if (!loaded.includes(def.family)) loaded.push(def.family) + }), + ) + } catch { + /* 单字体失败不阻塞整体 */ + } + } + } + await Promise.allSettled(tasks) + return loaded +} + +/** 生成内置字体的 @font-face CSS 块(baseUrl 用于拼字体绝对/相对路径) */ +export function builtinFontFaceCss(baseUrl: string): string { + return FONT_CATALOG.map((def) => + def.faces + .map((face) => { + const src = face.src.startsWith('/') ? `${baseUrl.replace(/\/+$/, '')}${face.src}` : face.src + const ext = src.split('?')[0]?.split('.').pop()?.toLowerCase() + const format = ext === 'ttf' ? 'truetype' : ext === 'otf' ? 'opentype' : ext === 'woff' ? 'woff' : 'woff2' + return ( + `@font-face{font-family:"${def.family}";` + + `src:url(${src}) format("${format}");` + + `font-weight:${face.weight ?? 'normal'};font-style:${face.style ?? 'normal'};}` + ) + }) + .join(''), + ).join('') +} + +/** 提取模板中实际用到的内置字体族(供导出/无头打印内联,避免全量内联) */ +export function templateUsedFonts(template: TemplateData): FontFamilyDef[] { + const used = new Set() + const walk = (comps: AnyControl[]): void => { + for (const c of comps) { + if (c.type === 'text') { + const family = (c as TextControl).style?.fontFamily + const def = findFontFamily(family) + if (def) used.add(def.family) + } + if (c.type === 'zone') walk(c.children) + } + } + for (const section of template.document.sections) walk(section.components as AnyControl[]) + return FONT_CATALOG.filter((f) => used.has(f.family)) +} + +/** 转为导出引擎的 FontFaceDef(data-URI 内联用) */ +export function toExportFontDefs(defs: FontFamilyDef[]): FontFaceDef[] { + return defs.flatMap((def) => + def.faces.map((face) => ({ + family: def.family, + src: face.src, + weight: face.weight, + style: face.style, + })), + ) +} diff --git a/openprint/src/core/headless/createHeadless.ts b/openprint/src/core/headless/createHeadless.ts new file mode 100644 index 0000000..1cec6a6 --- /dev/null +++ b/openprint/src/core/headless/createHeadless.ts @@ -0,0 +1,192 @@ +/** + * Headless 无头静默模式 —— 《OpenPrint-实施指南》Phase 9 / 《设计方案》§19.4.4 + * + * ## 定位 + * `createHeadless({ repository, dataSource, fonts })` 是一个**零 UI、不挂载任何可见 DOM** 的 + * 编程式渲染/导出/打印入口。它直接复用 `sdk.render()` + `export-engine`, + * 因此与「设计器预览」「浏览器打印」三者视觉完全一致(同一份 HTML/CSS 真相源)。 + * + * ## 与 Vue 解耦 + * 本文件是纯 TypeScript,不 import 任何 Vue / designer-vue 代码, + * 可直接被使用者的脚本、Node 参考渲染器或自动化测试调用。 + * + * ## 零隐藏 DOM 的保证 + * - 不往页面挂载任何隐藏预览节点(对比:设计器预览面板用独立 iframe)。 + * - `print()` 仅在打印瞬间创建一个 0 尺寸 iframe、注入 srcdoc、调用 `print()`, + * 打印完成(或用户取消)后立即移除,页面不留残留 DOM。 + * - 导出走 canvas/Blob,完全在内存与离屏 canvas 中完成。 + */ +import { render as sdkRender, type RenderRequest, type RenderResponse } from '@/core/sdk' +import { renderHtml } from '@/core/renderer-html' +import { + exportDocument, + type ExportFormat, + type ExportOptions, + type ExportOutcome, + type FontFaceDef, +} from '@/core/export-engine' +import { embedFontsInHtml, loadFonts } from './loader' +import type { TemplateRepository } from '@/repository/types' +import type { TemplateData } from '@/types/template' +import type { AnyControl } from '@/types/control' + +export interface HeadlessOptions { + /** 模板仓库(取模板用)。不传则 `buildRequest` 抛错,但 `render/exportXxx` 仍可手动传 template。 */ + repository?: TemplateRepository + /** 数据源仓库(保留位,当前用于约定签名;自动合成数据由调用方用 buildPreviewData 组合)。 */ + dataSource?: unknown + /** 同源自定义字体,导出 / 打印时嵌入,避免栅格化丢字体。 */ + fonts?: FontFaceDef[] +} + +/** 渲染/导出请求:模板 + 可选数据(未给数据则由调用方保证模板无 dataSource 依赖) */ +export interface HeadlessRequest { + template: TemplateData + data?: Record + layout?: RenderRequest['layout'] + output?: RenderRequest['output'] +} + +export interface HeadlessExportOptions { + /** 高清倍率 1/2/3,默认 2 */ + scale?: number + /** 文件名(不含扩展名) */ + filename?: string + /** 位图背景色,默认白 */ + background?: string +} + +export interface HeadlessInstance { + /** 仅排版,返回 HTML 产物 + 页面模型 + 告警 */ + render(req: HeadlessRequest): Promise + /** 导出 PDF(单文件多页,思源宋体写死内联) */ + exportPdf(req: HeadlessRequest, opts?: HeadlessExportOptions): Promise + /** 导出 JPG(多页每页一个文件) */ + exportJpg(req: HeadlessRequest, opts?: HeadlessExportOptions): Promise + /** 导出 SVG(单文件多页纵向堆叠矢量) */ + exportSvg(req: HeadlessRequest, opts?: HeadlessExportOptions): Promise + /** 静默浏览器打印(弹出系统打印对话框,零 UI 残留) */ + print(req: HeadlessRequest): Promise + /** 按模板 id 从 repository 取出模板,组装成 RenderRequest(默认 output.screen=false) */ + buildRequest(templateId: string, data?: Record): Promise + /** 释放资源(反注册字体等) */ + dispose(): void +} + +export function createHeadless(options: HeadlessOptions = {}): HeadlessInstance { + const fonts = options.fonts ?? [] + let teardownFonts: (() => void) | null = null + + // 浏览器环境:把字体注册到文档,供同源预览/打印使用 + if (fonts.length && typeof document !== 'undefined') { + loadFonts(fonts) + .then((t) => { + teardownFonts = t + }) + .catch(() => { + /* 字体加载失败不影响其余功能,栅格化会走系统字体兜底 */ + }) + } + + const toRequest = (req: HeadlessRequest): RenderRequest => ({ + template: req.template, + data: req.data, + layout: req.layout, + output: { screen: false, ...req.output }, + }) + + async function buildRequest(templateId: string, data?: Record): Promise { + if (!options.repository) throw new Error('createHeadless 未配置 repository,无法按 id 取模板') + const rec = await options.repository.get(templateId) + if (!rec) throw new Error(`模板不存在:${templateId}`) + return { template: rec.data, data, output: { screen: false } } + } + + async function render(req: HeadlessRequest): Promise { + return sdkRender(toRequest(req)) + } + + async function exportFormat( + req: HeadlessRequest, + format: ExportFormat, + opts?: HeadlessExportOptions, + ): Promise { + const expOpts: ExportOptions = { + fonts, + scale: opts?.scale, + filename: opts?.filename, + background: opts?.background, + } + return exportDocument(toRequest(req), format, expOpts) + } + + async function print(req: HeadlessRequest): Promise { + const res = await render(toRequest(req)) + const pageDecoration = toRequest(req).output?.pageDecoration + let html = renderHtml(res.result, { screen: false, pageDecoration }) + if (fonts.length) html = await embedFontsInHtml(html, fonts) + await silentPrint(html) + } + + function dispose(): void { + if (teardownFonts) { + try { + teardownFonts() + } catch { + /* noop */ + } + teardownFonts = null + } + } + + return { + render, + exportPdf: (r, o) => exportFormat(r, 'pdf', o), + exportJpg: (r, o) => exportFormat(r, 'jpg', o), + exportSvg: (r, o) => exportFormat(r, 'svg', o), + print, + buildRequest, + dispose, + } +} + +/** 静默打印:瞬态 0 尺寸 iframe + srcdoc + window.print(),完成后立即移除 */ +function silentPrint(html: string): Promise { + return new Promise((resolve, reject) => { + if (typeof document === 'undefined') { + reject(new Error('silentPrint 需要浏览器环境')) + return + } + const iframe = document.createElement('iframe') + iframe.setAttribute('aria-hidden', 'true') + iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;' + let removed = false + const cleanup = () => { + if (removed) return + removed = true + setTimeout(() => iframe.remove(), 300) + } + iframe.onload = () => { + const cw = iframe.contentWindow + if (!cw) { + cleanup() + reject(new Error('iframe 无 contentWindow')) + return + } + // onafterprint 在用户确认打印或取消时都会触发 + cw.onafterprint = () => { + cleanup() + resolve() + } + try { + cw.focus() + cw.print() + } catch (e) { + cleanup() + reject(e instanceof Error ? e : new Error(String(e))) + } + } + document.body.appendChild(iframe) + iframe.srcdoc = html + }) +} diff --git a/openprint/src/core/headless/headless.spec.ts b/openprint/src/core/headless/headless.spec.ts new file mode 100644 index 0000000..80fb1bb --- /dev/null +++ b/openprint/src/core/headless/headless.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' + +import { createHeadless } from '@/core/headless' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => ({ + productCode: `P${i + 1}`, + productName: `商品${i + 1}`, + qty: (i % 5) + 1, + price: 10, + amount: ((i % 5) + 1) * 10, + })) + return { order: { orderNo: 'SO-001' }, items } +} + +describe('createHeadless —— 无头静默模式', () => { + const measurer = createCjkMeasurer() + + it('render 复用 sdk 产出多页 HTML(零 UI 残留)', async () => { + const hl = createHeadless({ fonts: [] }) + const res = await hl.render({ template: createDemoTemplate(), data: makeData(30), layout: { measurer } }) + expect(res.pages).toBeGreaterThanOrEqual(2) + hl.dispose() + }) + + it('buildRequest 未配置 repository 时抛错(契约边界)', async () => { + const hl = createHeadless({ fonts: [] }) + await expect(hl.buildRequest('missing')).rejects.toThrow() + hl.dispose() + }) +}) diff --git a/openprint/src/core/headless/index.ts b/openprint/src/core/headless/index.ts new file mode 100644 index 0000000..a41d498 --- /dev/null +++ b/openprint/src/core/headless/index.ts @@ -0,0 +1,21 @@ +/** + * Headless 无头模式入口 —— 汇总导出 + * + * 用法: + * ```ts + * import { createHeadless } from '@/core/headless' + * const headless = createHeadless({ repository, dataSource, fonts }) + * const { blobs, filenames } = await headless.exportPdf({ template, data }, { scale: 2 }) + * blobs.forEach((b, i) => downloadBlob(b, filenames[i])) + * headless.dispose() + * ``` + */ +export { createHeadless } from './createHeadless' +export type { + HeadlessOptions, + HeadlessRequest, + HeadlessInstance, + HeadlessExportOptions, +} from './createHeadless' +export { loadFonts, embedFontsInSvg, embedFontsInHtml } from './loader' +export type { FontFaceDef } from '@/core/export-engine/fonts' diff --git a/openprint/src/core/headless/loader.ts b/openprint/src/core/headless/loader.ts new file mode 100644 index 0000000..4318aac --- /dev/null +++ b/openprint/src/core/headless/loader.ts @@ -0,0 +1,34 @@ +/** + * Headless 字体加载器 + * + * - re-export `export-engine/fonts` 的嵌字工具(让栅格化 / 打印文档用上自定义字体) + * - `loadFonts`:用 FontFace API 把同源字体注册到**当前文档**, + * 供设计器预览 / 浏览器打印的同源文档(iframe srcdoc)使用。 + * + * 注意:`loadFonts` 注册的是「文档级」字体,预览/打印文档只要同源就能用; + * 而栅格化(SVG ``)是隔离上下文,必须用 `embedFontsInSvg` 把字体写进 SVG 本身。 + */ +export { + type FontFaceDef, + embedFontsInSvg, + embedFontsInHtml, +} from '@/core/export-engine/fonts' + +/** 把同源字体注册到当前文档(供同源预览 / 打印使用)。返回反注册函数。 */ +export async function loadFonts(defs: import('@/core/export-engine/fonts').FontFaceDef[]): Promise<() => void> { + if (typeof document === 'undefined' || !('fonts' in document)) return () => {} + const fontsApi = (document as unknown as { fonts: { add(f: FontFace): void; delete(f: FontFace): void } }).fonts + const faces: FontFace[] = [] + for (const d of defs) { + const ff = new FontFace(d.family, `url(${d.src})`, { + weight: d.weight !== undefined ? String(d.weight) : 'normal', + style: d.style ?? 'normal', + }) + await ff.load() + fontsApi.add(ff) + faces.push(ff) + } + return () => { + for (const ff of faces) fontsApi.delete(ff) + } +} diff --git a/openprint/src/core/layout-engine/aggregate.spec.ts b/openprint/src/core/layout-engine/aggregate.spec.ts new file mode 100644 index 0000000..2cea3af --- /dev/null +++ b/openprint/src/core/layout-engine/aggregate.spec.ts @@ -0,0 +1,388 @@ +import { describe, expect, it } from 'vitest' + +import { + aggValueForRows, + columnAggregatable, + footerKindOf, + formatAggNumber, + isAggToken, + parseAggToken, + stripItems, + toChineseCapitalRMB, +} from '@/core/layout-engine/aggregate' +import { buildTableModel, sliceTable } from '@/core/layout-engine/table-engine' +import { + buildDesignGrid, + insertTableColumn, + insertTableRow, + rowRoleLabel, + seedSummaryTail, +} from '@/core/layout-engine/table-cells' +import type { TableCell, TableColumn, TableControl } from '@/types/control' +import type { EvalContext } from './types' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +/* ============================ 纯函数 ============================ */ + +describe('parseAggToken / isAggToken', () => { + it('识别各类聚合 token', () => { + expect(parseAggToken('{{#pageSum}}')).toBe('pageSum') + expect(parseAggToken('{{#totalSum}}')).toBe('totalSum') + expect(parseAggToken('{{#pageCap}}')).toBe('pageCap') + expect(parseAggToken('{{#totalCap}}')).toBe('totalCap') + expect(parseAggToken('{{#pageAvg}}')).toBe('pageAvg') + expect(parseAggToken('{{ #totalCount }}')).toBe('totalCount') + }) + it('非 token 返回 null', () => { + expect(parseAggToken('{{item.qty}}')).toBeNull() + expect(parseAggToken('本页合计')).toBeNull() + expect(parseAggToken('{{order.total}}')).toBeNull() + expect(isAggToken('{{#totalSum}}')).toBe(true) + expect(isAggToken('总量')).toBe(false) + }) + it('footerKindOf 归类', () => { + expect(footerKindOf('pageSum')).toBe('pageSubtotal') + expect(footerKindOf('totalSum')).toBe('grandTotal') + expect(footerKindOf('totalCap')).toBe('capital') + expect(footerKindOf(null)).toBe('static') + }) +}) + +describe('stripItems', () => { + it('去除 items[]. 前缀', () => { + expect(stripItems('items[].amount')).toBe('amount') + expect(stripItems('order.no')).toBe('order.no') + expect(stripItems(undefined)).toBe('') + }) +}) + +describe('aggValueForRows', () => { + const rows = [ + { qty: 2, price: 3 }, + { qty: 3, price: 4 }, + ] + it('求和', () => { + expect(aggValueForRows('totalSum', rows, 'qty')).toBe(5) + expect(aggValueForRows('pageSum', rows, 'price')).toBe(7) + }) + it('平均', () => { + expect(aggValueForRows('totalAvg', rows, 'price')).toBe(3.5) + }) + it('计数', () => { + expect(aggValueForRows('totalCount', rows, 'qty')).toBe(2) + }) +}) + +describe('formatAggNumber', () => { + it('整数不带小数,小数保留两位并加千分位', () => { + expect(formatAggNumber(18)).toBe('18') + expect(formatAggNumber(22650)).toBe('22,650') + expect(formatAggNumber(18.5)).toBe('18.50') + expect(formatAggNumber(1234567.5)).toBe('1,234,567.50') + }) +}) + +describe('toChineseCapitalRMB', () => { + it('整数大写', () => { + expect(toChineseCapitalRMB(22650)).toBe('贰万贰仟陆佰伍拾元整') + expect(toChineseCapitalRMB(18)).toBe('壹拾捌元整') + expect(toChineseCapitalRMB(100)).toBe('壹佰元整') + expect(toChineseCapitalRMB(1000000)).toBe('壹佰万元整') + }) + it('小数(角/分)', () => { + expect(toChineseCapitalRMB(100.05)).toBe('壹佰元零伍分') + expect(toChineseCapitalRMB(100.5)).toBe('壹佰元伍角') + expect(toChineseCapitalRMB(0.05)).toBe('伍分') + }) + it('零与负数', () => { + expect(toChineseCapitalRMB(0)).toBe('零元整') + expect(toChineseCapitalRMB(-22650)).toBe('负贰万贰仟陆佰伍拾元整') + }) +}) + +describe('columnAggregatable', () => { + it('显式 aggregate 优先', () => { + expect(columnAggregatable({ title: 'x', field: 'a', width: 10, aggregate: false }, undefined)).toBe(false) + expect(columnAggregatable({ title: 'x', field: 'a', width: 10, aggregate: true }, undefined)).toBe(true) + }) + it('按数据首行采样推断数值列', () => { + const col = { title: '金额', field: 'items[].amount', width: 10 } + expect(columnAggregatable(col, { amount: 5 })).toBe(true) + expect(columnAggregatable(col, { amount: 'abc' })).toBe(false) + expect(columnAggregatable({ title: '名称', field: 'items[].name', width: 10 }, { name: 'x' })).toBe(false) + }) +}) + +/* ====================== 引擎:尾行聚合集成 ====================== */ + +function mkTailTable(): TableControl { + const columns: TableColumn[] = [ + { title: '序号', expression: '{{rowIndex + 1}}', width: 15, align: 'center' }, + { title: '名称', field: 'items[].name', width: 60 }, + { title: '数量', field: 'items[].qty', width: 25, align: 'right', aggregate: true }, + { title: '单价', field: 'items[].price', width: 30, align: 'right', aggregate: true }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right', aggregate: true }, + ] + const cells: TableCell[][] = [ + [ + { text: '序号' }, + { text: '名称' }, + { text: '数量' }, + { text: '单价' }, + { text: '金额' }, + ], + [ + { expression: '{{rowIndex + 1}}' }, + { field: 'items[].name' }, + { field: 'items[].qty' }, + { field: 'items[].price' }, + { field: 'items[].amount' }, + ], + [ + { text: '本页合计', style: { bold: true } }, + {}, + { text: '{{#pageSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#pageSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#pageSum}}', style: { bold: true, align: 'right' } }, + ], + [ + { text: '总计', style: { bold: true } }, + {}, + { text: '{{#totalSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#totalSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#totalSum}}', style: { bold: true, align: 'right' } }, + ], + [ + { text: '大写金额', style: { bold: true } }, + {}, + {}, + {}, + { text: '{{#totalCap}}', style: { bold: true } }, + ], + ] + const data = [ + { name: 'a', qty: 1, price: 10, amount: 10 }, + { name: 'b', qty: 2, price: 10, amount: 20 }, + { name: 'c', qty: 3, price: 10, amount: 30 }, + { name: 'd', qty: 4, price: 10, amount: 40 }, + { name: 'e', qty: 5, price: 10, amount: 50 }, + ] + return { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns, + cells, + headerRows: 1, + staticRows: 3, + data, + options: { repeatHeader: true, repeatFooter: true, pageRows: 'auto', borders: 'all' }, + } +} + +function aggCellText(footer: { cells: { text?: string }[] }, col: number): string { + return footer.cells[col]?.text ?? '' +} + +describe('buildTableModel —— 尾行聚合结构', () => { + const measurer = createCjkMeasurer() + const ctx: EvalContext = { data: { items: [] } } + const model = buildTableModel({ control: mkTailTable(), ctx, measurer, widthMm: 160, heightMm: 60 }) + + it('尾行被识别为 pageSubtotal / grandTotal / capital', () => { + expect(model.footerRows).toHaveLength(3) + expect(model.footerRows[0]!.footerKind).toBe('pageSubtotal') + expect(model.footerRows[1]!.footerKind).toBe('grandTotal') + expect(model.footerRows[2]!.footerKind).toBe('capital') + }) + + it('数值列 token 被标记 isAgg + aggField,字符串列不计算', () => { + const pageRow = model.footerRows[0]! + // 数量列(index2)应为聚合单元格 + expect(pageRow.cells[2]!.isAgg).toBe(true) + expect(pageRow.cells[2]!.aggField).toBe('qty') + expect(pageRow.cells[2]!.tokenKind).toBe('pageSum') + // 名称列(index1)无 token + expect(pageRow.cells[1]!.isAgg).toBeFalsy() + expect(pageRow.cells[1]!.text).toBe('') + }) + + it('单页切片:本页合计=总计=全量,大写金额为整表总计', () => { + const slice = sliceTable(model, { avail: 1000, start: 0 }) + expect(slice.isLast).toBe(true) + const f = slice.footerRows + expect(f).toHaveLength(3) + // 数量:1+2+3+4+5=15 + expect(aggCellText(f[0]!, 2)).toBe('15') + expect(aggCellText(f[1]!, 2)).toBe('15') + // 金额:10+20+30+40+50=150 + expect(aggCellText(f[0]!, 4)).toBe('150') + expect(aggCellText(f[1]!, 4)).toBe('150') + // 大写金额 = 壹佰伍拾元整 + expect(aggCellText(f[2]!, 4)).toBe('壹佰伍拾元整') + }) +}) + +describe('sliceTable —— 强制分页时本页合计逐页、总计仅末页', () => { + const measurer = createCjkMeasurer() + const ctx: EvalContext = { data: { items: [] } } + const control: TableControl = { ...mkTailTable(), options: { repeatHeader: true, repeatFooter: true, pageRows: 2, borders: 'all' } } + const model = buildTableModel({ control, ctx, measurer, widthMm: 160, heightMm: 60 }) + + it('第 1 页(非末页)只有本页合计,不含总计/大写', () => { + const s1 = sliceTable(model, { avail: 1000, start: 0 }) + expect(s1.isLast).toBe(false) + // 本页合计 = 前 2 行:数量 1+2=3,金额 10+20=30 + expect(aggCellText(s1.footerRows[0]!, 2)).toBe('3') + expect(aggCellText(s1.footerRows[0]!, 4)).toBe('30') + // 末页专属行不出现 + expect(s1.footerRows.find((r) => r.footerKind === 'grandTotal')).toBeUndefined() + expect(s1.footerRows.find((r) => r.footerKind === 'capital')).toBeUndefined() + }) + + it('末页同时含本页合计、总计与大写金额', () => { + // 5 行 / 每页 2 → 第 3 页(start=4)为末页,仅 1 行 + const s3 = sliceTable(model, { avail: 1000, start: 4 }) + expect(s3.isLast).toBe(true) + expect(s3.footerRows.find((r) => r.footerKind === 'grandTotal')).toBeTruthy() + expect(s3.footerRows.find((r) => r.footerKind === 'capital')).toBeTruthy() + // 末页本页合计 = 第 5 行:数量 5,金额 50 + expect(aggCellText(s3.footerRows[0]!, 2)).toBe('5') + expect(aggCellText(s3.footerRows[0]!, 4)).toBe('50') + // 总计仍为全量 15 / 150 + const grand = s3.footerRows.find((r) => r.footerKind === 'grandTotal')! + expect(aggCellText(grand, 2)).toBe('15') + expect(aggCellText(grand, 4)).toBe('150') + // 大写金额取整表金额总计 + const cap = s3.footerRows.find((r) => r.footerKind === 'capital')! + expect(aggCellText(cap, 4)).toBe('壹佰伍拾元整') + }) +}) + +/* ====================== seedSummaryTail / 行角色 ====================== */ + +describe('seedSummaryTail', () => { + it('自动植入三行尾结构并标记数值列 aggregate', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '数量', field: 'items[].qty', width: 25, align: 'right' }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right' }, + ], + headerRows: 1, + data: [ + { name: 'a', qty: 1, amount: 10 }, + { name: 'b', qty: 2, amount: 20 }, + ], + options: { repeatHeader: true, repeatFooter: true, pageRows: 'auto', borders: 'all' }, + } + const seeded = seedSummaryTail(base) + const grid = buildDesignGrid(seeded) + expect(grid.rowCount).toBe(5) // 1 表头 + 1 数据 + 3 尾 + // 金额列(index2)被标记 aggregate + expect(seeded.columns[2]!.aggregate).toBe(true) + // 尾行含 token + const lastRows = grid.cells.slice(grid.headerRows + 1) + expect(lastRows.some((r) => r.some((c) => c.text === '{{#pageSum}}'))).toBe(true) + expect(lastRows.some((r) => r.some((c) => c.text === '{{#totalCap}}'))).toBe(true) + }) + + it('幂等:已植入则不重复', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right' }, + ], + headerRows: 1, + data: [{ name: 'a', amount: 1 }], + options: { pageRows: 'auto' }, + } + const once = seedSummaryTail(base) + const twice = seedSummaryTail(once) + expect(buildDesignGrid(twice).rowCount).toBe(buildDesignGrid(once).rowCount) + }) +}) + +describe('rowRoleLabel', () => { + it('聚合尾行返回正确角色名', () => { + const seeded = seedSummaryTail({ + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right' }, + ], + headerRows: 1, + data: [{ name: 'a', amount: 1 }], + options: { pageRows: 'auto' }, + }) + const grid = buildDesignGrid(seeded) + expect(rowRoleLabel(grid, 0)).toBe('标题行') + expect(rowRoleLabel(grid, 1)).toBe('数据行') + // 尾行:本页合计 / 总计 / 大写金额 + expect(rowRoleLabel(grid, 2)).toBe('本页合计行') + expect(rowRoleLabel(grid, 3)).toBe('总计行') + expect(rowRoleLabel(grid, 4)).toBe('大写金额行') + }) +}) + +describe('insertTableRow / insertTableColumn', () => { + it('插入行后行数+1,数据表维持单数据样例行', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [{ title: 'A', field: 'items[].a', width: 30 }], + headerRows: 1, + data: [{ a: 1 }], + options: { pageRows: 'auto' }, + } + const seeded = seedSummaryTail(base) // 5 行 + const withRow = insertTableRow(seeded, 3) // 在第 3 行(本页合计)上方插入 + const grid = buildDesignGrid(withRow) + expect(grid.rowCount).toBe(6) + expect(grid.staticRows).toBe(4) + }) + + it('插入列后列数+1,每行单元格同步扩展', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [{ title: 'A', field: 'items[].a', width: 30 }], + headerRows: 1, + data: [{ a: 1 }], + options: { pageRows: 'auto' }, + } + const withCol = insertTableColumn(base, 1) // 在列 1 左侧插入 + expect(withCol.columns).toHaveLength(2) + const grid = buildDesignGrid(withCol) + expect(grid.colCount).toBe(2) + expect(grid.cells.every((r) => r.length === 2)).toBe(true) + }) +}) diff --git a/openprint/src/core/layout-engine/aggregate.ts b/openprint/src/core/layout-engine/aggregate.ts new file mode 100644 index 0000000..9f1af23 --- /dev/null +++ b/openprint/src/core/layout-engine/aggregate.ts @@ -0,0 +1,212 @@ +/** + * aggregate —— 表格尾行(本页合计 / 总计 / 大写金额)聚合引擎(纯函数,可测) + * + * 设计要点(与用户需求对齐): + * - 表格默认结构 = 标题行 + 数据行 + 本页合计行 + 总计行 + 大写金额行。 + * - 尾行的「计算单元格」用约定表达式占位: + * - `{{#pageSum}}` 本页合计(每页只算本页数据行) + * - `{{#totalSum}}` 总计(整表所有数据行,仅末页出现) + * - `{{#pageCap}}` 本页合计大写金额 + * - `{{#totalCap}}` 总计大写金额(如 贰万贰仟陆佰伍拾元整,仅末页出现) + * - 另有 `{{#pageAvg}}`/`{{#totalAvg}}`/`{{#pageCount}}`/`{{#totalCount}}` 可用。 + * - 单元格里凡不是聚合 token 的 `{{}}` 视为普通插值;完全不含 `{{}}` 的字面量视为静态文本。 + * - 哪一列需要合计:由列字段类型(或数据行首行采样)判断,非数值列(字符串等)不计算。 + * - 合计整数不带小数、小数保留两位;大写金额按人民币大写规则转换。 + */ +import type { TableColumn } from '@/types/control' + +/* -------------------------------- Token 解析 -------------------------------- */ + +export type AggKind = + | 'pageSum' + | 'totalSum' + | 'pageAvg' + | 'totalAvg' + | 'pageCount' + | 'totalCount' + | 'pageCap' + | 'totalCap' + +/** 解析单元格文本是否为聚合 token;返回具体类型或 null */ +export function parseAggToken(text: string | undefined): AggKind | null { + if (!text) return null + const m = /^\{\{\s*#(pageSum|totalSum|pageAvg|totalAvg|pageCount|totalCount|pageCap|totalCap)\s*\}\}$/.exec( + text.trim(), + ) + return m ? (m[1] as AggKind) : null +} + +/** 该文本是否为聚合 token */ +export function isAggToken(text: string | undefined): boolean { + return parseAggToken(text) !== null +} + +/** token 所在的尾行类别 */ +export type FooterKind = 'pageSubtotal' | 'grandTotal' | 'capital' | 'static' + +export function footerKindOf(kind: AggKind | null): FooterKind { + if (kind === 'pageCap' || kind === 'totalCap') return 'capital' + if (kind === 'totalSum' || kind === 'totalAvg' || kind === 'totalCount') return 'grandTotal' + if (kind === 'pageSum' || kind === 'pageAvg' || kind === 'pageCount') return 'pageSubtotal' + return 'static' +} + +/* -------------------------------- 字段工具 -------------------------------- */ + +/** 去 `items[].` 前缀,得到数据行字段键(items[].amount → amount) */ +export function stripItems(field: string | undefined): string { + if (!field) return '' + return field.includes('[].') ? field.slice(field.indexOf('[].') + 3) : field +} + +function toNum(v: unknown): number { + if (typeof v === 'number') return v + if (v === null || v === undefined || v === '') return 0 + const n = Number(v) + return Number.isNaN(n) ? 0 : n +} + +/* -------------------------------- 聚合求值 -------------------------------- */ + +export function sumField(rows: Array>, field: string): number { + let s = 0 + for (const r of rows) s += toNum(r[field]) + return s +} + +export function avgField(rows: Array>, field: string): number { + return rows.length ? sumField(rows, field) / rows.length : 0 +} + +/** 按 token 类别,对给定数据行集合在 field 上求值 */ +export function aggValueForRows( + kind: AggKind, + rows: Array>, + field: string, +): number { + switch (kind) { + case 'pageSum': + case 'totalSum': + return sumField(rows, field) + case 'pageAvg': + case 'totalAvg': + return avgField(rows, field) + case 'pageCount': + case 'totalCount': + return rows.length + default: + return 0 + } +} + +/** + * 聚合数值显示:整数不带小数(默认整数计算),小数保留两位,统一加千分位。 + * 对齐 table-engine 的 formatAggregate 行为;字符串等非数值列不计算(调用方应已跳过)。 + */ +export function formatAggNumber(v: number): string { + const fixed = Number.isInteger(v) ? v.toFixed(0) : v.toFixed(2) + const [int = '0', dec] = fixed.split('.') + const sign = int.startsWith('-') ? '-' : '' + const grouped = (sign ? int.slice(1) : int).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + return sign + grouped + (dec ? `.${dec}` : '') +} + +/* ------------------------------ 大写人民币转换 ------------------------------ */ + +const DIGITS = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'] +const SMALL_UNITS = ['', '拾', '佰', '仟'] +// 每 4 位一组的大单位:个 / 万 / 亿 / 兆 +const GROUP_UNITS = ['', '万', '亿', '兆'] + +/** 0..9999 → 中文(不含组单位),并返回本组是否含非零位 */ +function groupToChinese(num: number): { text: string; hasNonZero: boolean } { + const s = String(num).padStart(4, '0') + let result = '' + let zero = false + let hasNonZero = false + for (let i = 0; i < 4; i++) { + const d = Number(s[i]) + if (d === 0) { + zero = true + } else { + if (zero && result) result += DIGITS[0] + result += (DIGITS[d] ?? '') + (SMALL_UNITS[3 - i] ?? '') + zero = false + hasNonZero = true + } + } + return { text: result, hasNonZero } +} + +/** 非负整数 → 中文数字(正确处理 万/亿 分组与组间零) */ +function integerToChinese(n: number): string { + if (n === 0) return DIGITS[0] ?? '零' + if (n < 0) return '负' + integerToChinese(-n) + // 按 4 位拆组(高位在前) + const groups: number[] = [] + let rem = Math.floor(n) + while (rem > 0) { + groups.unshift(rem % 10000) + rem = Math.floor(rem / 10000) + } + let result = '' + for (let g = 0; g < groups.length; g++) { + const { text, hasNonZero } = groupToChinese(groups[g] ?? 0) + if (hasNonZero) { + result += text + (GROUP_UNITS[groups.length - 1 - g] ?? '') + } else if (g < groups.length - 1 && result && !result.endsWith(DIGITS[0] ?? '零')) { + // 整组为 0(且非最低组)→ 在高位与低位之间补一个"零"连接 + result += DIGITS[0] ?? '零' + } + } + return result +} + +/** + * 金额 → 中文大写人民币(如 22650 → 贰万贰仟陆佰伍拾元整)。 + * - 支持整数与两位小数(角/分),第三位小数四舍五入。 + * - 负数加「负」前缀;零金额返回「零元整」。 + */ +export function toChineseCapitalRMB(input: number): string { + if (!Number.isFinite(input)) return '' + const negative = input < 0 + const amount = Math.abs(Math.round(input * 100) / 100) + if (amount === 0) return '零元整' + + const intPart = Math.floor(amount) + const decPart = Math.round((amount - intPart) * 100) // 0..99 + const jiao = Math.floor(decPart / 10) + const fen = decPart % 10 + + let result = '' + if (intPart > 0) result += integerToChinese(intPart) + '元' + if (jiao === 0 && fen === 0) { + result += '整' + } else { + if (jiao > 0) result += (DIGITS[jiao] ?? '') + '角' + else if (intPart > 0) result += DIGITS[0] ?? '零' // 元后零角(如 100.05 → 壹佰元零伍分) + if (fen > 0) result += (DIGITS[fen] ?? '') + '分' + } + return (negative ? '负' : '') + result +} + +/* ------------------------------ 列聚合判定 ------------------------------ */ + +/** + * 某列是否参与聚合(计算合计)。 + * - 显式 `aggregate:false` → 不参与。 + * - 显式 `aggregate:true` 或 `'sum'`/`'avg'`/`'count'` → 参与。 + * - 否则按数据首行采样推断:列字段在数据行里出现数值则参与(字符串等类型不计算)。 + */ +export function columnAggregatable( + col: TableColumn | undefined, + sample: Record | undefined, +): boolean { + if (!col) return false + if (col.aggregate === false) return false + if (col.aggregate === true || col.aggregate === 'sum' || col.aggregate === 'avg' || col.aggregate === 'count') + return true + const key = stripItems(col.field) + if (key && sample && typeof sample[key] === 'number') return true + return false +} diff --git a/openprint/src/core/layout-engine/cell-format.spec.ts b/openprint/src/core/layout-engine/cell-format.spec.ts new file mode 100644 index 0000000..4d91b7e --- /dev/null +++ b/openprint/src/core/layout-engine/cell-format.spec.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import { formatCellValue } from './expression' +import type { CellFormat } from '@/types/control' + +describe('formatCellValue —— 单元格数据格式', () => { + it('undefined / none / text 回落默认 stringify', () => { + expect(formatCellValue(123, undefined)).toBe('123') + expect(formatCellValue(123, { kind: 'none' })).toBe('123') + expect(formatCellValue(123, { kind: 'text' })).toBe('123') + expect(formatCellValue(null, undefined)).toBe('') + }) + + it('date:按模板输出(年-月-日 / 年月日 / 美式)', () => { + const d = new Date(2026, 7, 11) // 2026-08-11(本地,稳定) + expect(formatCellValue(d, { kind: 'date', pattern: 'YYYY-MM-DD' })).toBe('2026-08-11') + expect(formatCellValue(d, { kind: 'date', pattern: 'YYYY年MM月DD日' })).toBe('2026年08月11日') + expect(formatCellValue(d, { kind: 'date', pattern: 'MM/DD/YYYY' })).toBe('08/11/2026') + }) + + it('date:带时间模板', () => { + const d = new Date(2026, 7, 11, 14, 30) + expect(formatCellValue(d, { kind: 'date', pattern: 'YYYY-MM-DD HH:mm' })).toBe('2026-08-11 14:30') + }) + + it('date:默认模板为 YYYY-MM-DD', () => { + const d = new Date(2026, 7, 11) + expect(formatCellValue(d, { kind: 'date' })).toBe('2026-08-11') + }) + + it('int:默认带千分位;thousands=false 不分组', () => { + expect(formatCellValue(1234567, { kind: 'int' })).toBe('1,234,567') + expect(formatCellValue(1234567, { kind: 'int', thousands: false })).toBe('1234567') + expect(formatCellValue(0, { kind: 'int' })).toBe('0') + }) + + it('decimal:按 digits 截断;千分位可关', () => { + expect(formatCellValue(1234.567, { kind: 'decimal', digits: 2 })).toBe('1,234.57') + expect(formatCellValue(1234.5, { kind: 'decimal', digits: 0 })).toBe('1,235') + expect(formatCellValue(1234.567, { kind: 'decimal', digits: 2, thousands: false })).toBe('1234.57') + }) + + it('currency:符号 + 千分位 + 小数位', () => { + expect(formatCellValue(12800.5, { kind: 'currency' })).toBe('¥12,800.50') + expect(formatCellValue(1200.5, { kind: 'currency', code: 'USD' })).toBe('$1,200.50') + expect(formatCellValue(9.9, { kind: 'currency', code: 'EUR', digits: 1 })).toBe('€9.9') + }) + + it('percent:数值 ×100 加 %', () => { + expect(formatCellValue(0.125, { kind: 'percent' })).toBe('12.50%') + expect(formatCellValue(0.125, { kind: 'percent', digits: 1 })).toBe('12.5%') + expect(formatCellValue(1, { kind: 'percent' })).toBe('100.00%') + }) + + it('digits 越界被收敛到 [0,6]', () => { + expect(formatCellValue(1.23456789, { kind: 'decimal', digits: 99 })).toBe('1.234568') + expect(formatCellValue(1.23456789, { kind: 'decimal', digits: -5 })).toBe('1') + }) + + it('非数字输入按 0 处理(与引擎 toNum 一致)', () => { + expect(formatCellValue('abc', { kind: 'int' })).toBe('0') + expect(formatCellValue('', { kind: 'decimal' })).toBe('0.00') + }) +}) + +/** 类型守卫:确保 CellFormat 联合在编译期被消费(防止误删成员) */ +const _kinds: CellFormat['kind'][] = ['none', 'text', 'date', 'int', 'decimal', 'currency', 'percent'] +void _kinds diff --git a/openprint/src/core/layout-engine/data-binder.spec.ts b/openprint/src/core/layout-engine/data-binder.spec.ts new file mode 100644 index 0000000..0080692 --- /dev/null +++ b/openprint/src/core/layout-engine/data-binder.spec.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest' +import { resolveTextValue, resolveControlContent } from './data-binder' +import type { TextControl } from '@/types/control' +import type { EvalContext } from './types' + +function text(partial: Partial): TextControl { + return { id: 't1', type: 'text', left: 0, top: 0, width: 50, height: 10, ...partial } +} + +describe('resolveTextValue —— 文本控件格式化', () => { + const ctx: EvalContext = { data: { order: { date: '2026-08-11', total: 12800.5, qty: 1234.5, rate: 0.125 } } } + + it('无 format 时回落默认 stringify', () => { + const r = resolveTextValue(text({ binding: 'order.total' }), ctx) + expect(r.text).toBe('12800.5') + }) + + it('日期格式', () => { + const r = resolveTextValue( + text({ binding: 'order.date', format: { kind: 'date', pattern: 'YYYY年MM月DD日' } }), + ctx, + ) + expect(r.text).toBe('2026年08月11日') + }) + + it('整数 + 千分位', () => { + const r = resolveTextValue( + text({ binding: 'order.qty', format: { kind: 'int', thousands: true } }), + ctx, + ) + expect(r.text).toBe('1,235') + }) + + it('小数 + 千分位', () => { + const r = resolveTextValue( + text({ binding: 'order.total', format: { kind: 'decimal', digits: 2, thousands: true } }), + ctx, + ) + expect(r.text).toBe('12,800.50') + }) + + it('货币', () => { + const r = resolveTextValue( + text({ binding: 'order.total', format: { kind: 'currency', code: 'USD', digits: 2, thousands: true } }), + ctx, + ) + expect(r.text).toBe('$12,800.50') + }) + + it('百分比', () => { + const r = resolveTextValue( + text({ binding: 'order.rate', format: { kind: 'percent', digits: 1 } }), + ctx, + ) + expect(r.text).toBe('12.5%') + }) + + it('expression 模式不被 format 影响(过滤器优先)', () => { + const r = resolveTextValue( + text({ expression: '{{order.total | currency:\'CNY\'}}', format: { kind: 'int' } }), + ctx, + ) + expect(r.text).toContain('12,800.50') + }) +}) + +describe('resolveControlContent —— 文本格式贯穿渲染', () => { + const ctx: EvalContext = { data: { order: { date: '2026-08-11' } } } + + it('绑定字段按 format 输出(走 content 解析总入口)', async () => { + const { content, warnings } = await resolveControlContent( + text({ id: 't2', binding: 'order.date', format: { kind: 'date', pattern: 'MM/DD/YYYY' } }), + ctx, + ) + expect(content.kind).toBe('text') + if (content.kind === 'text') expect(content.text).toBe('08/11/2026') + expect(warnings).toEqual([]) + }) +}) diff --git a/openprint/src/core/layout-engine/data-binder.ts b/openprint/src/core/layout-engine/data-binder.ts new file mode 100644 index 0000000..b5ec747 --- /dev/null +++ b/openprint/src/core/layout-engine/data-binder.ts @@ -0,0 +1,248 @@ +/** + * DataBinder —— 控件 + 数据 → 渲染就绪内容 + * 真理源:《OpenPrint-设计方案.md》§5.2 / §5.4a(printable)/ §5.6(visibleIf)/ §5.7 / §5.8 + * + * 职责边界: + * - 只负责"内容是什么",不负责"放在哪一页"(那是 pagination-engine 的事) + * - 所有异步资源(二维码 SVG)在此解析完成,后续渲染全同步 + * - 条码/二维码统一输出 **SVG 矢量**(§5.7):打印用位图会糊,扫码枪可能读不出 + */ +import * as BwipJs from '@bwip-js/generic' +import DOMPurify from 'dompurify' +import QRCode from 'qrcode' + +import type { + AnyControl, + BarcodeControl, + ChartControl, + ImageControl, + QrcodeControl, + RichTextControl, + TextControl, +} from '@/types/control' +import { interpolate, evaluateVisible, resolveBinding, stringifyValue, formatCellValue } from './expression' +import type { EvalContext, ResolvedContent, RenderWarning } from './types' +import { renderChartControl } from '@/core/chartkit' + +/* ------------------------------ 可见性判定 ------------------------------ */ + +/** + * 渲染期是否输出该控件。 + * §5.4a:`printable:false` 渲染时跳过,但**设计画布仍可编辑**(所以只在这里过滤)。 + */ +export function isControlPrintable(control: AnyControl, ctx: EvalContext): boolean { + if (control.printable === false) return false + return evaluateVisible(control.visibleIf, ctx) +} + +/* -------------------------------- 文本 -------------------------------- */ + +/** + * 文本取值优先级:expression > binding > value。 + * 注意 `value` 里也允许写 `{{}}`(用户在属性面板静态文本里手打变量是常见操作), + * 所以静态文本同样过一遍插值。 + */ +export function resolveTextValue( + control: TextControl, + ctx: EvalContext, +): { text: string; errors: string[] } { + if (control.expression) return interpolate(control.expression, ctx) + if (control.binding) { + // binding 模式:按控件 format 格式化(expression 模式请用 {{field | date:'...'}} 过滤器) + const raw = resolveBinding(control.binding, ctx) + return { text: formatCellValue(raw, control.format), errors: [] } + } + return interpolate(control.value ?? '', ctx) +} + +/* -------------------------------- 图片 -------------------------------- */ + +function resolveImageSrc(control: ImageControl, ctx: EvalContext): string { + const v = control.value + if (!v || !v.content) return '' + switch (v.mode) { + case 'binding': { + const raw = resolveBinding(v.content, ctx) + return raw === null || raw === undefined ? '' : String(raw) + } + case 'inline': + case 'url': + case 'asset': + default: + return v.content + } +} + +/* ------------------------------ 条码 / 二维码 ----------------------------- */ + +function resolveCodeText(control: BarcodeControl | QrcodeControl, ctx: EvalContext): string { + if (control.binding) { + const v = resolveBinding(control.binding, ctx) + if (v !== null && v !== undefined && v !== '') return String(v) + } + if (control.value) return interpolate(control.value, ctx).text + // 既没 binding 也没 value:设计画布兜底示例码,预览同样兜底,避免空白 + return control.type === 'qrcode' ? 'https://openprint.dev' : '0123456789' +} + +/** + * 让 SVG 自适应控件尺寸:去掉固定 width/height,保留 viewBox 由外层容器缩放。 + * bwip-js / qrcode 输出的 SVG 都带写死的 width/height,直接嵌入会溢出控件框。 + */ +function makeSvgResponsive(svg: string): string { + return svg + .replace(/]*?)\swidth="[^"]*"/i, ']*?)\sheight="[^"]*"/i, ' { + const svg = await QRCode.toString(text, { + type: 'svg', + errorCorrectionLevel: control.errorLevel ?? 'M', + margin: 0, + }) + return makeSvgResponsive(svg) +} + +/* ------------------------------- 富文本消毒 ------------------------------ */ + +/** §11.2:Renderer 输出前必须对绑定内容消毒,防模板注入 XSS */ +function sanitizeRichText(html: string): string { + return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }) +} + +/* ------------------------------ 内容解析总入口 ----------------------------- */ + +export interface ResolvedControlContent { + content: ResolvedContent + warnings: RenderWarning[] +} + +/** + * 把单个控件解析为渲染就绪内容。 + * 任何失败都降级为 placeholder + warning —— **绝不静默丢内容**, + * 否则用户拿到一张缺字段的单据却毫不知情,这在 ERP 场景是事故。 + */ +export async function resolveControlContent( + control: AnyControl, + ctx: EvalContext, +): Promise { + const warnings: RenderWarning[] = [] + + switch (control.type) { + case 'text': { + const { text, errors } = resolveTextValue(control, ctx) + for (const message of errors) { + warnings.push({ code: 'EXPRESSION_ERROR', message, controlId: control.id }) + } + if (!text && control.binding) { + warnings.push({ + code: 'BINDING_MISSING', + message: `字段 "${control.binding}" 在数据中为空`, + controlId: control.id, + }) + } + return { content: { kind: 'text', text }, warnings } + } + + case 'richtext': { + const raw = (control as RichTextControl).value ?? '' + const { text, errors } = interpolate(raw, ctx) + for (const message of errors) { + warnings.push({ code: 'EXPRESSION_ERROR', message, controlId: control.id }) + } + return { content: { kind: 'html', html: sanitizeRichText(text) }, warnings } + } + + case 'image': { + const src = resolveImageSrc(control, ctx) + if (!src) { + warnings.push({ + code: 'IMAGE_UNRESOLVED', + message: '图片来源为空', + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '图片' }, warnings } + } + return { content: { kind: 'image', src }, warnings } + } + + case 'barcode': { + const text = resolveCodeText(control, ctx) + if (!text) { + warnings.push({ code: 'BARCODE_FAILED', message: '条码内容为空', controlId: control.id }) + return { content: { kind: 'placeholder', label: '条码' }, warnings } + } + try { + return { content: { kind: 'svg', svg: renderBarcodeSvg(control, text) }, warnings } + } catch (e) { + warnings.push({ + code: 'BARCODE_FAILED', + message: `条码生成失败(${control.format ?? 'code128'}):${e instanceof Error ? e.message : String(e)}`, + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '条码' }, warnings } + } + } + + case 'qrcode': { + const text = resolveCodeText(control, ctx) + if (!text) { + warnings.push({ code: 'BARCODE_FAILED', message: '二维码内容为空', controlId: control.id }) + return { content: { kind: 'placeholder', label: '二维码' }, warnings } + } + try { + return { content: { kind: 'svg', svg: await renderQrcodeSvg(control, text) }, warnings } + } catch (e) { + warnings.push({ + code: 'BARCODE_FAILED', + message: `二维码生成失败:${e instanceof Error ? e.message : String(e)}`, + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '二维码' }, warnings } + } + } + + case 'rect': + case 'line': + return { content: { kind: 'shape' }, warnings } + + case 'chart': { + // 原生 SVG 图表(零依赖):与条码同理走矢量 svg,打印不糊。 + try { + return { content: { kind: 'svg', svg: renderChartControl(control as ChartControl) }, warnings } + } catch (e) { + warnings.push({ + code: 'CHART_FAILED', + message: `图表生成失败:${e instanceof Error ? e.message : String(e)}`, + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '图表' }, warnings } + } + } + + default: + // table / zone 不走这里(由 table-engine / pagination-engine 处理) + return { content: { kind: 'placeholder', label: control.type }, warnings } + } +} diff --git a/openprint/src/core/layout-engine/expression.spec.ts b/openprint/src/core/layout-engine/expression.spec.ts new file mode 100644 index 0000000..2a6e0ed --- /dev/null +++ b/openprint/src/core/layout-engine/expression.spec.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest' + +import { + evaluate, + evaluateVisible, + interpolate, + resolveBinding, +} from '@/core/layout-engine/expression' + +// EvalContext 必须有 data 字段;测试里无变量时用 { data: {} } + + +describe('expression 安全表达式引擎', () => { + it('基础算术与优先级', () => { + expect(evaluate('1 + 2 * 3', { data: {} })).toBe(7) + expect(evaluate('(1 + 2) * 3', { data: {} })).toBe(9) + expect(evaluate('10 / 4', { data: {} })).toBe(2.5) + }) + + it('字符串拼接', () => { + expect(evaluate('"a" + "b"', { data: {} })).toBe('ab') + expect(evaluate('"x" + 1', { data: {} })).toBe('x1') + }) + + it('比较与逻辑', () => { + expect(evaluate('3 > 2', { data: {} })).toBe(true) + expect(evaluate('true && false', { data: {} })).toBe(false) + expect(evaluate('true || false', { data: {} })).toBe(true) + expect(evaluate('!false', { data: {} })).toBe(true) + }) + + it('变量从 ctx.data 取值', () => { + expect(evaluate('x * 2', { data: { x: 5 } })).toBe(10) + expect(evaluate('a + b', { data: { a: 2, b: 3 } })).toBe(5) + }) + + it('过滤器(经 interpolate 解析 {{ }} 管道)', () => { + expect(interpolate('{{name | upper}}', { data: { name: 'abc' } }).text).toBe('ABC') + expect(interpolate('{{name | lower}}', { data: { name: 'ABC' } }).text).toBe('abc') + expect(interpolate('{{v | number:0}}', { data: { v: 1234.5 } }).text).toBe('1,235') + expect(interpolate('{{amount | currency}}', { data: { amount: 12800.5 } }).text).toContain('12,800.50') + expect(interpolate('{{v | default:"无"}}', { data: { v: '' } }).text).toBe('无') + expect(interpolate('{{n | padStart:3}}', { data: { n: 7 } }).text).toBe('007') + }) + + it('resolveBinding 按路径取值', () => { + expect(resolveBinding('a.b', { data: { a: { b: 1 } } })).toBe(1) + }) + + it('interpolate 替换 {{ }}', () => { + expect(interpolate('{{x}}岁', { data: { x: 18 } }).text).toBe('18岁') + expect(interpolate('单价{{price|currency}}', { data: { price: 9.9 } }).text).toContain('9.90') + expect(interpolate('无占位', { data: {} }).text).toBe('无占位') + }) + + it('evaluateVisible 条件渲染', () => { + expect(evaluateVisible('', { data: {} })).toBe(true) + expect(evaluateVisible(' ', { data: {} })).toBe(true) + expect(evaluateVisible('false', { data: {} })).toBe(false) + expect(evaluateVisible('x > 3', { data: { x: 5 } })).toBe(true) + expect(evaluateVisible('x > 3', { data: { x: 1 } })).toBe(false) + }) + + // —— 安全边界:绝不能执行任意 JS —— + it('拒绝 eval / new Function / import 等代码注入', () => { + expect(() => evaluate('eval("x")', { data: {} })).toThrow() + expect(() => evaluate('new Function("return 1")', { data: {} })).toThrow() + expect(() => evaluate('import("x")', { data: {} })).toThrow() + expect(() => evaluate('window.location', { data: {} })).not.toThrow() // 读取未定义变量返回 undefined,而非执行 + expect(evaluate('window.location', { data: {} })).toBeUndefined() + }) + + it('语法错误抛出', () => { + expect(() => evaluate('1 +', { data: {} })).toThrow() + expect(() => evaluate('@#$', { data: {} })).toThrow() + }) +}) diff --git a/openprint/src/core/layout-engine/expression.ts b/openprint/src/core/layout-engine/expression.ts new file mode 100644 index 0000000..5740315 --- /dev/null +++ b/openprint/src/core/layout-engine/expression.ts @@ -0,0 +1,751 @@ +/** + * 表达式引擎 —— 绑定取值 / 插值 / 条件渲染的统一入口 + * 真理源:《OpenPrint-设计方案.md》§5.3(表达式文本)、§5.6(条件渲染)、§5.4(列 expression) + * + * ## 为什么不用 mustache? + * §11.2 建议用 mustache 做 `resolveBinding()`,但 mustache 是**逻辑无关**模板引擎: + * 它无法求值 `{{rowIndex + 1}}`、`{{row.price * row.qty}}`(§5.4 列表达式的原文示例), + * 也不支持 `{{order.total | currency:'CNY'}}` 的过滤器语法(§5.3 原文示例)。 + * 而 `new Function` / `eval` 方案在 ERP 场景不可接受(模板可能来自不可信的第三方设计者, + * 且企业环境常配 CSP `script-src` 白名单,eval 直接被拦)。 + * + * 因此这里实现一个**受限安全求值器**: + * - 纯 Pratt 解析 + 树求值,无 new Function / eval / with + * - 白名单运算符,无函数调用、无属性赋值、无原型访问(__proto__ / constructor 被拒) + * - 只读取传入的 EvalContext,够不到全局对象 + * 覆盖面严格 ⊇ mustache 的用途,且顺带解决 visibleIf,一个引擎服务三处。 + */ +import type { EvalContext } from './types' +import type { CellFormat } from '@/types/control' + +/* ================================ 词法分析 ================================ */ + +type TokenType = 'num' | 'str' | 'ident' | 'op' | 'eof' + +interface Token { + type: TokenType + value: string + pos: number +} + +/** 多字符运算符按长度降序,保证 `===` 不被切成 `==` + `=` */ +const OPERATORS = [ + '===', + '!==', + '==', + '!=', + '>=', + '<=', + '&&', + '||', + '??', + '+', + '-', + '*', + '/', + '%', + '>', + '<', + '!', + '?', + ':', + '(', + ')', + '[', + ']', + '.', + ',', +] + +function tokenize(src: string): Token[] { + const tokens: Token[] = [] + let i = 0 + while (i < src.length) { + const ch = src[i]! + + // 空白 + if (/\s/.test(ch)) { + i++ + continue + } + + // 字符串字面量(单/双引号,支持反斜杠转义) + if (ch === "'" || ch === '"') { + const quote = ch + let out = '' + let j = i + 1 + while (j < src.length && src[j] !== quote) { + if (src[j] === '\\' && j + 1 < src.length) { + out += src[j + 1] + j += 2 + } else { + out += src[j] + j++ + } + } + tokens.push({ type: 'str', value: out, pos: i }) + i = j + 1 + continue + } + + // 数字(含小数) + if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(src[i + 1] ?? ''))) { + let j = i + while (j < src.length && /[0-9.]/.test(src[j]!)) j++ + tokens.push({ type: 'num', value: src.slice(i, j), pos: i }) + i = j + continue + } + + // 标识符(含中文字段名,ERP 里很常见) + if (/[A-Za-z_$\u4e00-\u9fa5]/.test(ch)) { + let j = i + while (j < src.length && /[A-Za-z0-9_$\u4e00-\u9fa5]/.test(src[j]!)) j++ + tokens.push({ type: 'ident', value: src.slice(i, j), pos: i }) + i = j + continue + } + + // 运算符 + const op = OPERATORS.find((o) => src.startsWith(o, i)) + if (op) { + tokens.push({ type: 'op', value: op, pos: i }) + i += op.length + continue + } + + throw new Error(`表达式非法字符 "${ch}"(位置 ${i})`) + } + tokens.push({ type: 'eof', value: '', pos: i }) + return tokens +} + +/* ================================ 语法分析 ================================ */ + +type Node = + | { t: 'lit'; v: unknown } + | { t: 'path'; segs: Array } + | { t: 'un'; op: string; a: Node } + | { t: 'bin'; op: string; a: Node; b: Node } + | { t: 'cond'; c: Node; a: Node; b: Node } + +/** 二元运算符优先级(数字越大结合越紧) */ +const BIN_PREC: Record = { + '??': 1, + '||': 2, + '&&': 3, + '===': 4, + '!==': 4, + '==': 4, + '!=': 4, + '<': 5, + '<=': 5, + '>': 5, + '>=': 5, + '+': 6, + '-': 6, + '*': 7, + '/': 7, + '%': 7, +} + +/** 禁止访问的属性名 —— 防原型链逃逸 */ +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']) + +class Parser { + private pos = 0 + constructor(private readonly tokens: Token[]) {} + + private peek(): Token { + return this.tokens[this.pos]! + } + + private next(): Token { + return this.tokens[this.pos++]! + } + + private eat(value: string): boolean { + if (this.peek().type === 'op' && this.peek().value === value) { + this.pos++ + return true + } + return false + } + + private expect(value: string): void { + if (!this.eat(value)) { + throw new Error(`表达式缺少 "${value}"(位置 ${this.peek().pos})`) + } + } + + parse(): Node { + const node = this.parseTernary() + if (this.peek().type !== 'eof') { + throw new Error(`表达式多余内容 "${this.peek().value}"`) + } + return node + } + + /** 三元(右结合,优先级最低) */ + private parseTernary(): Node { + const cond = this.parseBinary(0) + if (this.eat('?')) { + const a = this.parseTernary() + this.expect(':') + const b = this.parseTernary() + return { t: 'cond', c: cond, a, b } + } + return cond + } + + /** 优先级爬升 */ + private parseBinary(minPrec: number): Node { + let left = this.parseUnary() + for (;;) { + const tok = this.peek() + if (tok.type !== 'op') break + const prec = BIN_PREC[tok.value] + if (prec === undefined || prec < minPrec) break + this.next() + const right = this.parseBinary(prec + 1) // 全部左结合 + left = { t: 'bin', op: tok.value, a: left, b: right } + } + return left + } + + private parseUnary(): Node { + const tok = this.peek() + if (tok.type === 'op' && (tok.value === '!' || tok.value === '-' || tok.value === '+')) { + this.next() + return { t: 'un', op: tok.value, a: this.parseUnary() } + } + return this.parsePrimary() + } + + private parsePrimary(): Node { + const tok = this.next() + + if (tok.type === 'num') return { t: 'lit', v: Number(tok.value) } + if (tok.type === 'str') return { t: 'lit', v: tok.value } + + if (tok.type === 'op' && tok.value === '(') { + const node = this.parseTernary() + this.expect(')') + return node + } + + if (tok.type === 'ident') { + if (tok.value === 'true') return { t: 'lit', v: true } + if (tok.value === 'false') return { t: 'lit', v: false } + if (tok.value === 'null') return { t: 'lit', v: null } + if (tok.value === 'undefined') return { t: 'lit', v: undefined } + return this.parsePath(tok.value) + } + + throw new Error(`表达式意外符号 "${tok.value}"(位置 ${tok.pos})`) + } + + /** 路径:a.b[0].c,以及协议里的数组标记 a[] */ + private parsePath(head: string): Node { + const segs: Array = [head] + for (;;) { + if (this.eat('.')) { + const tok = this.next() + if (tok.type !== 'ident' && tok.type !== 'num') { + throw new Error(`路径 "." 后应为字段名(位置 ${tok.pos})`) + } + segs.push(tok.value) + continue + } + if (this.eat('[')) { + // `items[]` —— 协议里表示"该数组本身",跳过不产生下标段 + if (this.eat(']')) continue + const idx = this.parseTernary() + this.expect(']') + segs.push(idx) + continue + } + break + } + return { t: 'path', segs } + } +} + +/* ================================= 求值 ================================= */ + +function looseEqual(a: unknown, b: unknown): boolean { + // 避免 eslint eqeqeq;语义等价于 ==(仅做数字/字符串宽松比较) + if (a === b) return true + if (a === null || a === undefined || b === null || b === undefined) { + return (a === null || a === undefined) && (b === null || b === undefined) + } + if (typeof a === 'number' || typeof b === 'number') return Number(a) === Number(b) + return String(a) === String(b) +} + +function toNum(v: unknown): number { + if (typeof v === 'number') return v + if (v === null || v === undefined || v === '') return 0 + const n = Number(v) + return Number.isNaN(n) ? 0 : n +} + +function isTruthy(v: unknown): boolean { + if (Array.isArray(v)) return v.length > 0 + return Boolean(v) +} + +function readProp(obj: unknown, key: string): unknown { + if (obj === null || obj === undefined) return undefined + if (FORBIDDEN_KEYS.has(key)) return undefined + if (typeof obj !== 'object' && typeof obj !== 'string') return undefined + // 数组的 length 允许;字符串的 length 允许 + return (obj as Record)[key] +} + +function evalNode(node: Node, ctx: EvalContext, scope?: Record): unknown { + switch (node.t) { + case 'lit': + return node.v + + case 'path': { + const [head, ...rest] = node.segs as [string, ...Array] + let cur: unknown = resolveRoot(head, ctx, scope) + for (const seg of rest) { + if (cur === null || cur === undefined) return undefined + const key = typeof seg === 'string' ? seg : String(evalNode(seg, ctx, scope)) + cur = readProp(cur, key) + } + return cur + } + + case 'un': { + const v = evalNode(node.a, ctx) + if (node.op === '!') return !isTruthy(v) + if (node.op === '-') return -toNum(v) + return toNum(v) + } + + case 'cond': + return isTruthy(evalNode(node.c, ctx)) ? evalNode(node.a, ctx) : evalNode(node.b, ctx) + + case 'bin': { + // 短路运算符先处理 + if (node.op === '&&') { + const a = evalNode(node.a, ctx, scope) + return isTruthy(a) ? evalNode(node.b, ctx, scope) : a + } + if (node.op === '||') { + const a = evalNode(node.a, ctx, scope) + return isTruthy(a) ? a : evalNode(node.b, ctx, scope) + } + if (node.op === '??') { + const a = evalNode(node.a, ctx, scope) + return a === null || a === undefined ? evalNode(node.b, ctx, scope) : a + } + + const a = evalNode(node.a, ctx, scope) + const b = evalNode(node.b, ctx, scope) + switch (node.op) { + case '+': + // 任一侧为字符串则拼接,符合直觉("单号-" + order.no) + if (typeof a === 'string' || typeof b === 'string') return String(a ?? '') + String(b ?? '') + return toNum(a) + toNum(b) + case '-': + return toNum(a) - toNum(b) + case '*': + return toNum(a) * toNum(b) + case '/': { + const d = toNum(b) + return d === 0 ? 0 : toNum(a) / d + } + case '%': { + const d = toNum(b) + return d === 0 ? 0 : toNum(a) % d + } + case '===': + return a === b + case '!==': + return a !== b + case '==': + return looseEqual(a, b) + case '!=': + return !looseEqual(a, b) + case '<': + return toNum(a) < toNum(b) + case '<=': + return toNum(a) <= toNum(b) + case '>': + return toNum(a) > toNum(b) + case '>=': + return toNum(a) >= toNum(b) + default: + throw new Error(`不支持的运算符 "${node.op}"`) + } + } + } +} + +/** + * 路径根解析。约定(与字段树 path 对齐): + * - `data` → 数据根(§5.6 的 `data.vip === true`) + * - `row` → 当前行;`rowIndex` → 行号;`page` / `pages` → 页码变量 + * - 其他标识符 → 先在 row 里找(表格单元格里 `qty` 即 `row.qty`),再退到数据根 + */ +function resolveRoot(head: string, ctx: EvalContext, scope?: Record): unknown { + // 自定义作用域(如合计表达式里的 sum/avg/rows/allRows)优先于数据根解析 + if (scope && head in scope) return readProp(scope, head) + switch (head) { + case 'data': + return ctx.data + case 'row': + return ctx.row + case 'rowIndex': + return ctx.rowIndex ?? 0 + // 页码变量的多种别名,模板设计者不必记忆唯一写法 + case 'page': + case 'pageNo': + case 'pageNumber': + return ctx.page ?? 1 + case 'pages': + case 'pageCount': + case 'totalPages': + return ctx.pages ?? 1 + default: + if (ctx.row && head in (ctx.row as object)) return readProp(ctx.row, head) + return readProp(ctx.data, head) + } +} + +/* =============================== 编译缓存 =============================== */ + +const astCache = new Map() + +function compile(src: string): Node { + const hit = astCache.get(src) + if (hit instanceof Error) throw hit + if (hit) return hit + try { + const ast = new Parser(tokenize(src)).parse() + astCache.set(src, ast) + return ast + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)) + astCache.set(src, err) + throw err + } +} + +/** + * 求值单个表达式(不含 {{}})。失败时抛错,由调用方转 warning。 + * @param scope 可选额外作用域(如自定义合计里的 sum/avg/rows/allRows),标识符解析时优先于数据根。 + */ +export function evaluate(src: string, ctx: EvalContext, scope?: Record): unknown { + return evalNode(compile(src), ctx, scope) +} + +/** 条件渲染判定(§5.6)。表达式为空视为可见;求值失败**保守可见**,避免静默丢内容。 */ +export function evaluateVisible(src: string | undefined, ctx: EvalContext): boolean { + if (!src || !src.trim()) return true + try { + return isTruthy(evaluate(src, ctx)) + } catch { + return true + } +} + +/* ================================ 过滤器 ================================ */ + +export type Filter = (value: unknown, ...args: unknown[]) => string + +const CURRENCY_SYMBOL: Record = { + CNY: '¥', + RMB: '¥', + USD: '$', + EUR: '€', + GBP: '£', + JPY: '¥', + HKD: 'HK$', +} + +function groupThousands(fixed: string): string { + const [int = '0', dec] = fixed.split('.') + const sign = int.startsWith('-') ? '-' : '' + const digits = sign ? int.slice(1) : int + const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + return sign + grouped + (dec ? `.${dec}` : '') +} + +function pad2(n: number): string { + return String(n).padStart(2, '0') +} + +function formatDate(value: unknown, pattern = 'YYYY-MM-DD'): string { + if (value === null || value === undefined || value === '') return '' + const d = value instanceof Date ? value : new Date(String(value)) + if (Number.isNaN(d.getTime())) return String(value) + const map: Record = { + YYYY: String(d.getFullYear()), + MM: pad2(d.getMonth() + 1), + DD: pad2(d.getDate()), + HH: pad2(d.getHours()), + mm: pad2(d.getMinutes()), + ss: pad2(d.getSeconds()), + } + return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (k) => map[k] ?? k) +} + +export const FILTERS: Record = { + /** {{ order.total | currency:'CNY' }} → ¥12,800.50 */ + currency(value, code = 'CNY', digits = 2) { + const symbol = CURRENCY_SYMBOL[String(code).toUpperCase()] ?? '' + return symbol + groupThousands(toNum(value).toFixed(toNum(digits))) + }, + /** {{ items[].qty | number:0 }} → 1,234 */ + number(value, digits = 2) { + return groupThousands(toNum(value).toFixed(toNum(digits))) + }, + /** 不加千分位的定点小数 */ + fixed(value, digits = 2) { + return toNum(value).toFixed(toNum(digits)) + }, + /** {{ order.rate | percent:1 }} → 12.5% */ + percent(value, digits = 2) { + return `${(toNum(value) * 100).toFixed(toNum(digits))}%` + }, + /** {{ order.orderDate | date:'YYYY年MM月DD日' }} */ + date(value, pattern = 'YYYY-MM-DD') { + return formatDate(value, String(pattern)) + }, + upper(value) { + return String(value ?? '').toUpperCase() + }, + lower(value) { + return String(value ?? '').toLowerCase() + }, + /** 空值兜底:{{ customer.memo | default:'无' }} */ + default(value, fallback = '') { + const empty = value === null || value === undefined || value === '' + return empty ? String(fallback) : String(value) + }, + /** 超长截断:{{ productName | truncate:10 }} */ + truncate(value, len = 20, suffix = '…') { + const s = String(value ?? '') + const n = toNum(len) + return s.length > n ? s.slice(0, n) + String(suffix) : s + }, + /** 补零:{{ seq | padStart:3 }} → 007 */ + padStart(value, len = 2, fill = '0') { + return String(value ?? '').padStart(toNum(len), String(fill)) + }, +} + +/* ============================== 单元格数据格式 ============================== */ + +/** 小数位收敛到 [0, 6],避免 digits 乱填导致 toFixed 抛错 */ +function clampDigits(d: unknown): number { + const n = typeof d === 'number' ? d : Number(d) + if (!Number.isFinite(n)) return 2 + return Math.min(6, Math.max(0, Math.trunc(n))) +} + +/** + * 按 CellFormat 把原始值格式化为显示字符串。 + * 复用 date/number/currency 的同一套底层工具(formatDate / groupThousands / CURRENCY_SYMBOL), + * 与 `{{field | date:'...' }}` 过滤器输出完全一致 —— UI 选择即等价于手写对应过滤器。 + * fmt 为 undefined / kind='none'/'text' 时回落默认 stringify(保持原行为)。 + */ +export function formatCellValue(raw: unknown, fmt?: CellFormat | null): string { + if (!fmt || fmt.kind === 'none' || fmt.kind === 'text') return stringifyValue(raw) + const n = toNum(raw) + switch (fmt.kind) { + case 'date': + return formatDate(raw, fmt.pattern ?? 'YYYY-MM-DD') + case 'int': { + const s = n.toFixed(0) + return fmt.thousands === false ? s : groupThousands(s) + } + case 'decimal': { + const s = n.toFixed(clampDigits(fmt.digits ?? 2)) + return fmt.thousands === false ? s : groupThousands(s) + } + case 'currency': { + const sym = CURRENCY_SYMBOL[String(fmt.code ?? 'CNY').toUpperCase()] ?? '' + const s = n.toFixed(clampDigits(fmt.digits ?? 2)) + return sym + (fmt.thousands === false ? s : groupThousands(s)) + } + case 'percent': + return `${(n * 100).toFixed(clampDigits(fmt.digits ?? 2))}%` + default: + return stringifyValue(raw) + } +} + +/* ============================== 插值({{}}) ============================== */ + +/** + * 按单竖线切分过滤器管道,需跳过: + * - 字符串字面量内的 `|` + * - 逻辑或 `||` + */ +function splitPipes(src: string): string[] { + const parts: string[] = [] + let buf = '' + let quote: string | null = null + for (let i = 0; i < src.length; i++) { + const ch = src[i]! + if (quote) { + buf += ch + if (ch === '\\' && i + 1 < src.length) { + buf += src[i + 1] + i++ + } else if (ch === quote) { + quote = null + } + continue + } + if (ch === "'" || ch === '"') { + quote = ch + buf += ch + continue + } + if (ch === '|') { + if (src[i + 1] === '|') { + buf += '||' + i++ + continue + } + parts.push(buf) + buf = '' + continue + } + buf += ch + } + parts.push(buf) + return parts +} + +/** 解析 `filterName:arg1,arg2`,参数走同一套求值器(可用字面量与路径) */ +function applyFilter(spec: string, value: unknown, ctx: EvalContext): string { + const trimmed = spec.trim() + const colon = trimmed.indexOf(':') + const name = (colon === -1 ? trimmed : trimmed.slice(0, colon)).trim() + const filter = FILTERS[name] + if (!filter) throw new Error(`未知过滤器 "${name}"`) + + let args: unknown[] = [] + if (colon !== -1) { + const argSrc = trimmed.slice(colon + 1) + args = splitArgs(argSrc).map((a) => evaluate(a, ctx)) + } + return filter(value, ...args) +} + +/** 按逗号切分过滤器参数,跳过字符串与括号内的逗号 */ +function splitArgs(src: string): string[] { + const out: string[] = [] + let buf = '' + let quote: string | null = null + let depth = 0 + for (let i = 0; i < src.length; i++) { + const ch = src[i]! + if (quote) { + buf += ch + if (ch === '\\' && i + 1 < src.length) { + buf += src[i + 1] + i++ + } else if (ch === quote) quote = null + continue + } + if (ch === "'" || ch === '"') { + quote = ch + buf += ch + continue + } + if (ch === '(' || ch === '[') depth++ + if (ch === ')' || ch === ']') depth-- + if (ch === ',' && depth === 0) { + out.push(buf) + buf = '' + continue + } + buf += ch + } + if (buf.trim()) out.push(buf) + return out +} + +/** 值 → 显示字符串的默认规则(数字保留原样,null/undefined 显示空) */ +export function stringifyValue(v: unknown): string { + if (v === null || v === undefined) return '' + if (typeof v === 'number') return Number.isInteger(v) ? String(v) : String(v) + if (typeof v === 'boolean') return v ? '是' : '否' + if (v instanceof Date) return formatDate(v) + if (typeof v === 'object') return JSON.stringify(v) + return String(v) +} + +export interface InterpolateResult { + text: string + /** 求值失败的片段(转 warning,不打断渲染) */ + errors: string[] +} + +/** + * 插值:把 `{{ expr | filter:arg }}` 替换为求值结果(§5.3)。 + * 非 `{{}}` 部分原样保留;单个片段求值失败时输出空串并记录错误。 + */ +export function interpolate(template: string, ctx: EvalContext): InterpolateResult { + const errors: string[] = [] + if (!template.includes('{{')) return { text: template, errors } + + const text = template.replace(/\{\{([\s\S]*?)\}\}/g, (_m, raw: string) => { + const body = raw.trim() + if (!body) return '' + try { + const [exprSrc, ...filterSpecs] = splitPipes(body) + let value: unknown = evaluate((exprSrc ?? '').trim(), ctx) + if (filterSpecs.length === 0) return stringifyValue(value) + let out = '' + for (let i = 0; i < filterSpecs.length; i++) { + out = applyFilter(filterSpecs[i]!, value, ctx) + value = out + } + return out + } catch (e) { + errors.push(`{{${body}}}:${e instanceof Error ? e.message : String(e)}`) + return '' + } + }) + + return { text, errors } +} + +/** + * 绑定路径取值(§5.2 的 `binding` 字段)。 + * 字段树给出的路径形如 `items[].qty`,其中 `[]` 由解析器忽略, + * 因此在行上下文中 `items[].qty` 与 `qty` 等价 —— 用户从字段树直接拖入即可用。 + */ +export function resolveBinding(path: string, ctx: EvalContext): unknown { + if (!path) return undefined + // 行上下文里优先按"去前缀"解释:items[].qty → qty + if (ctx.row) { + const tail = path.includes('[].') ? path.slice(path.indexOf('[].') + 3) : path + try { + const v = evaluate(tail, { ...ctx }) + if (v !== undefined) return v + } catch { + /* 落到全局解析 */ + } + } + try { + return evaluate(path, ctx) + } catch { + return undefined + } +} + +/** 测试/调试用:清空 AST 缓存 */ +export function __clearExpressionCache(): void { + astCache.clear() +} diff --git a/openprint/src/core/layout-engine/group-engine.spec.ts b/openprint/src/core/layout-engine/group-engine.spec.ts new file mode 100644 index 0000000..f2a8607 --- /dev/null +++ b/openprint/src/core/layout-engine/group-engine.spec.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' + +import { computeAggregates, subtotalLabel } from './group-engine' +import type { TableControl } from '@/types/control' + +const COLS_WITH_AMOUNT = [{ field: 'amount', title: '金额', width: 30 }] + +describe('computeAggregates —— 自定义合计(type=custom)', () => { + it('字段专属表达式:引用预计算的 sum', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { + type: 'custom' as const, + fields: ['amount'], + expressions: { amount: 'sum.amount * 2' }, + }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 10 }, { amount: 20 }], control) + expect(out.amount).toBe(60) // (10+20)*2 + }) + + it('无专属表达式时回退到 summaryRow.expression', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { type: 'custom' as const, fields: ['amount'], expression: 'sum.amount + 5' }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 10 }, { amount: 20 }], control) + expect(out.amount).toBe(35) // 30+5 + }) + + it('avg 作用域可用', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { type: 'custom' as const, fields: ['amount'], expressions: { amount: 'avg.amount' } }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 10 }, { amount: 20 }, { amount: 30 }], control) + expect(out.amount).toBe(20) + }) + + it('作用域含 rows / allRows(分组内行 / 整表行)', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { + type: 'custom' as const, + fields: ['amount'], + expressions: { amount: 'rows.length * 10 + allRows.length' }, + }, + }, + } as unknown as TableControl + // 当前分组 2 行,整表 3 行 → 2*10 + 3 = 23 + const out = computeAggregates( + [{ amount: 1 }, { amount: 2 }], + control, + [{ amount: 1 }, { amount: 2 }, { amount: 3 }], + ) + expect(out.amount).toBe(23) + }) + + it('无聚合列时把单表达式结果落入占位键 __total__', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { type: 'custom' as const, fields: [], expression: 'sum.amount + 1' }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 9 }], control) + expect(out.__total__).toBe(10) + }) +}) + +describe('subtotalLabel —— 模板替换', () => { + it('默认「${key} 小计」', () => { + const control = { columns: [], options: {} } as unknown as TableControl + expect(subtotalLabel(control, 'A组')).toBe('A组 小计') + }) + + it('自定义模板替换 ${key}', () => { + const control = { + columns: [], + options: { summaryRow: { subtotalLabel: '${key} 分组小计' } }, + } as unknown as TableControl + expect(subtotalLabel(control, '甲')).toBe('甲 分组小计') + }) +}) diff --git a/openprint/src/core/layout-engine/group-engine.ts b/openprint/src/core/layout-engine/group-engine.ts new file mode 100644 index 0000000..0fcc020 --- /dev/null +++ b/openprint/src/core/layout-engine/group-engine.ts @@ -0,0 +1,213 @@ +/** + * GroupEngine —— 分组与统计 + * 真理源:《OpenPrint-设计方案.md》§5.5(分组统计)、§5.4(summaryRow) + * + * 产物是「行计划」(RowPlan[]):一个把 组头 / 数据行 / 小计 / 总计 编排好的线性序列。 + * 之所以先出行计划再交给 TableEngine 测量,是为了让"分组语义"和"高度/分页"彻底解耦: + * 分页时只需按 RowPlan 顺序切,不必再理解分组结构。 + */ +import type { TableControl } from '@/types/control' +import { evaluate, resolveBinding, stringifyValue } from './expression' +import type { EvalContext, RenderRowKind } from './types' + +export type RowPlanKind = Extract + +export interface RowPlan { + kind: RowPlanKind + /** data 行的原始数据对象 */ + row?: Record + /** 在原始数组中的下标(斑马纹/序号用,跨分组连续) */ + dataIndex?: number + /** group 行的分组标题 / subtotal|summary 行的标签 */ + label?: string + /** 聚合结果:字段名 → 数值(供 subtotal / summary 行按列填值) */ + aggregates?: Record +} + +/* -------------------------------- 聚合 -------------------------------- */ + +function toNumber(v: unknown): number { + if (typeof v === 'number') return v + if (v === null || v === undefined || v === '') return 0 + const n = Number(v) + return Number.isNaN(n) ? 0 : n +} + +/** 从行对象里按字段名取值,兼容 `items[].amount` 这类带前缀的路径 */ +function readField(row: Record, field: string): unknown { + if (field in row) return row[field] + const tail = field.includes('[].') ? field.slice(field.indexOf('[].') + 3) : field + if (tail in row) return row[tail] + // 支持 a.b 嵌套 + return tail.split('.').reduce((acc, k) => { + if (acc === null || acc === undefined || typeof acc !== 'object') return undefined + return (acc as Record)[k] + }, row) +} + +export function aggregate( + rows: Array>, + type: 'sum' | 'count', + field: string, +): number { + if (type === 'count') return rows.length + return rows.reduce((sum, r) => sum + toNumber(readField(r, field)), 0) +} + +/** + * 计算一批行在若干字段上的聚合值。 + * 字段清单来自 `control.summary` 与 `options.summaryRow.fields` 的并集。 + * @param allRows 整表(跨分组)行,供 type='custom' 的表达式引用 `allRows`。 + */ +export function computeAggregates( + rows: Array>, + control: TableControl, + allRows?: Array>, +): Record { + const out: Record = {} + + for (const s of control.summary ?? []) { + out[s.field] = aggregate(rows, s.type, s.field) + } + + const sr = control.options?.summaryRow + if (!sr || sr.type === 'custom') { + // custom 类型在下方单独处理(sr 为 undefined 时也不走 sum/count 分支) + } else { + for (const f of sr.fields ?? []) { + out[f] = aggregate(rows, sr.type, f) + } + return out + } + + if (!sr) return out + + // ── type='custom':用自定义表达式求值,作用域含预计算的 sum/avg 与原始行 ── + const sum: Record = {} + const avg: Record = {} + for (const col of control.columns ?? []) { + const f = col.field + if (f) { + sum[f] = aggregate(rows, 'sum', f) + avg[f] = rows.length ? sum[f] / rows.length : 0 + } + } + const scope = { sum, avg, rows, allRows: allRows ?? rows } + const fields = sr.fields ?? [] + const evalCtx: EvalContext = { data: {} } + if (fields.length === 0) { + // 无聚合列:把单表达式结果放进占位列(渲染时标签列之前呈现) + try { + out['__total__'] = toNumber(evaluate(sr.expression ?? '0', evalCtx, scope)) + } catch { + /* 表达式错误时静默留 0,由调用方 warning 兜底 */ + } + } + for (const f of fields) { + const expr = sr.expressions?.[f] ?? sr.expression ?? '0' + try { + out[f] = toNumber(evaluate(expr, evalCtx, scope)) + } catch { + out[f] = 0 + } + } + return out +} + +/** 是否配置了任何合计行 */ +export function hasSummaryRow(control: TableControl): boolean { + const sr = control.options?.summaryRow + if (sr) return true + return (control.summary?.length ?? 0) > 0 +} + +/** 合计行标签(默认「合计」) */ +export function summaryLabel(control: TableControl): string { + return control.options?.summaryRow?.label ?? control.summary?.[0]?.label ?? '合计' +} + +/** 分组小计标签(默认「${key} 小计」,`${key}` 替换为分组值) */ +export function subtotalLabel(control: TableControl, key: string): string { + const tpl = control.options?.summaryRow?.subtotalLabel + return tpl ? tpl.replace(/\$\{key\}/g, key) : `${key} 小计` +} + +/* ------------------------------- 空行判定 ------------------------------- */ + +/** §5.4 skipEmptyRows:一行里所有被引用字段都为空则视为空行 */ +export function isEmptyRow(row: Record, fields: string[]): boolean { + if (fields.length === 0) return false + return fields.every((f) => { + const v = readField(row, f) + return v === null || v === undefined || v === '' + }) +} + +/* ------------------------------ 行计划编排 ------------------------------ */ + +export interface PlanOptions { + control: TableControl + rows: Array> + ctx: EvalContext +} + +/** + * 生成行计划。 + * + * 无 groupBy:`[data, data, ..., summary?]` + * 有 groupBy:`[group, data..., subtotal, group, data..., subtotal, ..., summary?]` + * + * 分组保持**首次出现顺序**(不重排),符合 ERP 单据"按录入顺序打印"的预期。 + */ +export function planRows({ control, rows, ctx }: PlanOptions): RowPlan[] { + const plans: RowPlan[] = [] + const groupBy = control.groupBy?.trim() + + if (!groupBy) { + rows.forEach((row, i) => plans.push({ kind: 'data', row, dataIndex: i })) + } else { + const buckets = new Map; index: number }>>() + rows.forEach((row, i) => { + const raw = resolveBinding(groupBy, { ...ctx, row, rowIndex: i }) + const key = stringifyValue(raw) + const list = buckets.get(key) + if (list) list.push({ row, index: i }) + else buckets.set(key, [{ row, index: i }]) + }) + + for (const [key, items] of buckets) { + plans.push({ kind: 'group', label: key || '(未分组)' }) + for (const { row, index } of items) { + plans.push({ kind: 'data', row, dataIndex: index }) + } + if (hasSummaryRow(control)) { + plans.push({ + kind: 'subtotal', + label: subtotalLabel(control, key || '(未分组)'), + aggregates: computeAggregates( + items.map((i) => i.row), + control, + rows, + ), + }) + } + } + } + + return plans +} + +/** 全表总计行(渲染为表尾,参与 repeatFooter 逻辑) */ +export function buildSummaryPlan( + rows: Array>, + control: TableControl, +): RowPlan | null { + if (!hasSummaryRow(control)) return null + return { + kind: 'summary', + label: summaryLabel(control), + aggregates: computeAggregates(rows, control, rows), + } +} + +export { readField } diff --git a/openprint/src/core/layout-engine/header-footer.ts b/openprint/src/core/layout-engine/header-footer.ts new file mode 100644 index 0000000..1278a65 --- /dev/null +++ b/openprint/src/core/layout-engine/header-footer.ts @@ -0,0 +1,117 @@ +/** + * HeaderFooterEngine + 页面度量 + * 真理源:《OpenPrint-设计方案.md》§5.1(sections)、§5.13(orientation 交换)、§5.14(ZoneControl)、§7.2(每页注入) + * + * 两件事: + * 1. computePageMetrics —— 把协议里的纸张定义换算成引擎唯一的尺寸真理源(全 mm) + * 2. buildSectionControls —— 把 header/footer 的子组件解析成每页可复用的已定位控件 + * + * 页码变量在**每页单独求值**(`{{page}} / {{pages}}`),所以页眉页脚不能只解析一次就复用, + * 必须按页重算 —— 这是"第 X 页 / 共 Y 页"能正确输出的关键。 + */ +import type { AnyControl } from '@/types/control' +import type { PageSetup, Section } from '@/types/template' +import { toMm } from '@/core/units' +import { isControlPrintable, resolveControlContent } from './data-binder' +import type { EvalContext, PageMetrics, PlacedControl, RenderWarning } from './types' + +/* ------------------------------- 页面度量 ------------------------------- */ + +/** + * 计算一页的可用区域。 + * 【约定】PageSetup.width/height 是**物理尺寸**(实际打印宽高,横向时宽>高), + * orientation 仅是派生标志(设计器面板切换方向时已交换宽高)。 + * 这里**不再**按 orientation 交换 —— 与设计器 setPage、导出 export-pdf(w>h 判定)同口径。 + */ +export function computePageMetrics( + page: PageSetup, + headerSection?: Section, + footerSection?: Section, +): PageMetrics { + const unit = page.unit + const pageWidth = toMm(page.width, unit) + const pageHeight = toMm(page.height, unit) + + const margin = { + top: toMm(page.margin?.top ?? 0, unit), + right: toMm(page.margin?.right ?? 0, unit), + bottom: toMm(page.margin?.bottom ?? 0, unit), + left: toMm(page.margin?.left ?? 0, unit), + } + + const contentWidth = Math.max(0, pageWidth - margin.left - margin.right) + const contentHeight = Math.max(0, pageHeight - margin.top - margin.bottom) + + const headerHeight = headerSection ? toMm(headerSection.height ?? 0, unit) : 0 + const footerHeight = footerSection ? toMm(footerSection.height ?? 0, unit) : 0 + const bodyHeight = Math.max(0, contentHeight - headerHeight - footerHeight) + + return { + pageWidth, + pageHeight, + margin, + contentWidth, + contentHeight, + headerHeight, + footerHeight, + bodyHeight, + } +} + +/* ------------------------------ 区块控件构建 ----------------------------- */ + +/** + * 把一个 section 的子组件解析成已定位控件。 + * 坐标已经是相对区块左上角(设计器序列化时保证),这里原样透传。 + */ +export async function buildSectionControls( + components: AnyControl[] | undefined, + ctx: EvalContext, + unit: PageSetup['unit'], +): Promise<{ controls: PlacedControl[]; warnings: RenderWarning[] }> { + const controls: PlacedControl[] = [] + const warnings: RenderWarning[] = [] + if (!components?.length) return { controls, warnings } + + for (const control of components) { + // zone 不应出现在 section.components 里(设计器已拆平),保险起见跳过 + if (control.type === 'zone') continue + if (!isControlPrintable(control, ctx)) continue + + const { content, warnings: w } = await resolveControlContent(control, ctx) + warnings.push(...w) + controls.push({ + kind: 'control', + id: control.id, + left: toMm(control.left, unit), + top: toMm(control.top, unit), + width: toMm(control.width, unit), + height: toMm(control.height, unit), + angle: control.angle, + content, + control, + }) + } + + return { controls, warnings } +} + +/* ------------------------------- 出现规则 ------------------------------- */ + +/** + * 区块是否在该页出现。 + * §5.1:`repeat` 默认 true(每页重复)。repeat=false 时按业界惯例: + * - 页眉只出现在**首页**(封面式抬头) + * - 页脚只出现在**末页**(签章/合计式收尾) + */ +export function shouldRenderSection( + section: Section | undefined, + type: 'header' | 'footer', + pageIndex: number, + totalPages: number, +): boolean { + if (!section) return false + const repeat = section.repeat ?? true + if (repeat) return true + return type === 'header' ? pageIndex === 0 : pageIndex === totalPages - 1 +} diff --git a/openprint/src/core/layout-engine/measure.ts b/openprint/src/core/layout-engine/measure.ts new file mode 100644 index 0000000..aef6224 --- /dev/null +++ b/openprint/src/core/layout-engine/measure.ts @@ -0,0 +1,231 @@ +/** + * 文本测量 —— 分页精度的关键模块 + * 真理源:《OpenPrint-设计方案.md》§11.3 + * + * | 环境 | 方案 | 精度 | + * |---|---|---| + * | 浏览器 | 真实 DOM 排版(本文件 DomMeasurer) | 像素精确,与最终打印一致 | + * | 无 DOM | CJK 感知估算(EstimateMeasurer) | 近似,仅作兜底 | + * + * §11.3 特别警告:**不要用 jsdom 估算** —— jsdom 不实现 CSS 布局引擎, + * getBoundingClientRect 永远返回 0,任何基于它的测量都是假精度。 + * 因此这里的兜底走字符宽度估算,而不是假装有 DOM。 + */ +import { mmToPx, ptToPx, pxToMm } from '@/core/units' + +export interface TextMeasureOptions { + /** 字号(pt,协议单位) */ + fontSize: number + fontFamily?: string + fontWeight?: 'normal' | 'bold' + /** 行高倍率(默认 1.35,贴近打印排版习惯) */ + lineHeight?: number + /** 字间距(pt) */ + letterSpacing?: number + /** 可用宽度(mm);<=0 表示不换行 */ + widthMm: number + /** 是否允许自动换行(默认 true) */ + wrap?: boolean +} + +export interface TextMeasureResult { + /** 文本占用高度(mm) */ + heightMm: number + /** 实际折行数 */ + lines: number +} + +export interface TextMeasurer { + measure(text: string, options: TextMeasureOptions): TextMeasureResult + /** 释放底层资源(DOM 节点等) */ + dispose(): void +} + +export const DEFAULT_FONT_FAMILY = + '"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", sans-serif' +export const DEFAULT_LINE_HEIGHT = 1.35 + +function cacheKey(text: string, o: TextMeasureOptions): string { + return [ + text, + o.fontSize, + o.fontFamily ?? '', + o.fontWeight ?? 'normal', + o.lineHeight ?? DEFAULT_LINE_HEIGHT, + o.letterSpacing ?? 0, + Math.round(o.widthMm * 100), + o.wrap === false ? 0 : 1, + ].join('|') +} + +/* ============================ 浏览器:真实 DOM ============================ */ + +/** + * 用一个离屏隐藏容器做真实排版测量。 + * + * 关键点: + * - 用 `visibility:hidden` + 绝对定位移出视口,**不能用 `display:none`**(不排版,高度恒 0) + * - `contain: layout size style` 让浏览器把它当独立布局单元,避免污染主文档回流 + * - `white-space: pre-wrap` + `word-break: break-word` 与渲染器输出的 CSS 严格一致, + * 否则"测量用一套规则、渲染用另一套"会导致分页错位 + */ +class DomMeasurer implements TextMeasurer { + private host: HTMLDivElement + private cache = new Map() + + constructor() { + const host = document.createElement('div') + host.setAttribute('aria-hidden', 'true') + host.dataset.openprint = 'measure' + Object.assign(host.style, { + position: 'absolute', + left: '-99999px', + top: '0', + visibility: 'hidden', + pointerEvents: 'none', + contain: 'layout size style', + padding: '0', + margin: '0', + border: '0', + } satisfies Partial) + document.body.appendChild(host) + this.host = host + } + + measure(text: string, options: TextMeasureOptions): TextMeasureResult { + const key = cacheKey(text, options) + const hit = this.cache.get(key) + if (hit) return hit + + const lineHeight = options.lineHeight ?? DEFAULT_LINE_HEIGHT + const fontPx = ptToPx(options.fontSize) + const el = this.host + + el.style.fontSize = `${fontPx}px` + el.style.fontFamily = options.fontFamily || DEFAULT_FONT_FAMILY + el.style.fontWeight = options.fontWeight ?? 'normal' + el.style.lineHeight = String(lineHeight) + el.style.letterSpacing = options.letterSpacing ? `${ptToPx(options.letterSpacing)}px` : 'normal' + + if (options.wrap === false || options.widthMm <= 0) { + el.style.width = 'auto' + el.style.whiteSpace = 'pre' + } else { + el.style.width = `${mmToPx(options.widthMm)}px` + el.style.whiteSpace = 'pre-wrap' + el.style.wordBreak = 'break-word' + el.style.overflowWrap = 'break-word' + } + + // 空串也要占一行高度,否则空单元格会把行压扁 + el.textContent = text === '' ? '\u00A0' : text + + const heightPx = el.getBoundingClientRect().height + const singleLinePx = fontPx * lineHeight + const result: TextMeasureResult = { + heightMm: pxToMm(heightPx), + lines: Math.max(1, Math.round(heightPx / Math.max(1, singleLinePx))), + } + + // 缓存上限,避免超长表格把内存吃满 + if (this.cache.size > 5000) this.cache.clear() + this.cache.set(key, result) + return result + } + + dispose(): void { + this.cache.clear() + this.host.remove() + } +} + +/* ============================== 无 DOM 兜底 ============================== */ + +/** + * 字符宽度估算:CJK / 全角按 1em,ASCII 按 0.5em(等宽近似)。 + * 精度不如 DOM,但至少不会像 jsdom 那样返回 0。 + */ +class EstimateMeasurer implements TextMeasurer { + private cache = new Map() + + private static charWidthEm(ch: string): number { + const code = ch.codePointAt(0) ?? 0 + // CJK 统一表意文字 / 全角标点 / 假名 / 韩文 + if ( + (code >= 0x1100 && code <= 0x115f) || + (code >= 0x2e80 && code <= 0xa4cf) || + (code >= 0xac00 && code <= 0xd7a3) || + (code >= 0xf900 && code <= 0xfaff) || + (code >= 0xfe30 && code <= 0xfe6f) || + (code >= 0xff00 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) + ) { + return 1 + } + return 0.5 + } + + measure(text: string, options: TextMeasureOptions): TextMeasureResult { + const key = cacheKey(text, options) + const hit = this.cache.get(key) + if (hit) return hit + + const lineHeight = options.lineHeight ?? DEFAULT_LINE_HEIGHT + const fontPx = ptToPx(options.fontSize) + const letterPx = options.letterSpacing ? ptToPx(options.letterSpacing) : 0 + const availPx = options.wrap === false || options.widthMm <= 0 ? Infinity : mmToPx(options.widthMm) + + let lines = 1 + let cursor = 0 + for (const ch of text) { + if (ch === '\n') { + lines++ + cursor = 0 + continue + } + const w = EstimateMeasurer.charWidthEm(ch) * fontPx + letterPx + if (cursor + w > availPx && cursor > 0) { + lines++ + cursor = w + } else { + cursor += w + } + } + + const result: TextMeasureResult = { + heightMm: pxToMm(lines * fontPx * lineHeight), + lines, + } + if (this.cache.size > 5000) this.cache.clear() + this.cache.set(key, result) + return result + } + + dispose(): void { + this.cache.clear() + } +} + +/* ================================ 工厂 ================================ */ + +let shared: TextMeasurer | null = null + +/** 创建测量器:有 DOM 走真实排版,无 DOM 走估算 */ +export function createMeasurer(): TextMeasurer { + const hasDom = typeof document !== 'undefined' && typeof document.body !== 'undefined' + return hasDom ? new DomMeasurer() : new EstimateMeasurer() +} + +/** + * 全局共享测量器(复用 DOM 节点 + 缓存,多次渲染成本近似 0)。 + * 渲染流程用这个;单测想要干净环境时自行 createMeasurer()。 + */ +export function getSharedMeasurer(): TextMeasurer { + if (!shared) shared = createMeasurer() + return shared +} + +export function disposeSharedMeasurer(): void { + shared?.dispose() + shared = null +} diff --git a/openprint/src/core/layout-engine/pagination-engine.spec.ts b/openprint/src/core/layout-engine/pagination-engine.spec.ts new file mode 100644 index 0000000..529b295 --- /dev/null +++ b/openprint/src/core/layout-engine/pagination-engine.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' + +import { layout } from '@/core/layout-engine/pagination-engine' +import { renderHtml } from '@/core/renderer-html' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + const price = 10 + (i % 9) + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price, + amount: qty * price, + } + }) + return { + order: { orderNo: 'SO-2026-0001', orderDate: '2026-08-08' }, + customer: { name: '演示客户' }, + items, + } +} + +describe('layout 分页引擎(集成 demo 模板)', () => { + const template = createDemoTemplate() + const measurer = createCjkMeasurer() + + for (const rows of [30, 80]) { + it(`明细 ${rows} 行 → 多页,页眉页脚每页重复,合计仅末页`, async () => { + const result = await layout(template, makeData(rows), { measurer }) + const pages = result.pages.length + expect(pages).toBeGreaterThanOrEqual(2) + + const html = renderHtml(result, { screen: false }) + // 样式表不参与计数 + const body = html.replace(//g, '') + + // 页眉页脚每页重复 + expect((body.match(/op-section op-header/g) || []).length).toBe(pages) + expect((body.match(/op-section op-footer/g) || []).length).toBe(pages) + + // 表格按页切片:每页一个
+ expect((body.match(/op-node op-table/g) || []).length).toBe(pages) + + // 合计行仅一次(末页) + expect((body.match(/is-summary/g) || []).length).toBe(1) + + // 零告警(数据完整、绑定可达) + expect(result.warnings.length).toBe(0) + }) + } +}) diff --git a/openprint/src/core/layout-engine/pagination-engine.ts b/openprint/src/core/layout-engine/pagination-engine.ts new file mode 100644 index 0000000..04632e0 --- /dev/null +++ b/openprint/src/core/layout-engine/pagination-engine.ts @@ -0,0 +1,572 @@ +/** + * PaginationEngine —— 分页算法主流程 + * 真理源:《OpenPrint-设计方案.md》§7.2(分页算法流程)、§7.4(难点清单)、§8.1(二维排版,非流式) + * + * ## 分页模型:绝对定位 + 单表流动 + * + * OpenPrint 是**二维排版工具,不是网页编辑器**(§8.1):正文控件都是相对区块左上角的 + * 绝对定位盒子,本身不参与文档流。真正会"长高"的只有绑定了数据源的表格。 + * 因此分页的语义是: + * + * ``` + * ┌── 表格上方控件(单据头:单号/客户/日期) → 仅第 1 页 + * ├── 明细表格 → 按行切片流过 N 页 + * └── 表格下方控件(单据尾:合计/签章) → 仅最后一页,随表格实际高度下移 + * ``` + * + * 这正是 ERP 单据的真实结构,也是金蝶/帆软等报表工具的一致做法。 + * 每页重复的内容(公司抬头、页码)交给 header/footer 区块(§5.14),不在正文里解决。 + * + * 无表格时退化为「按位置切页」:控件按 top 落在第几个 bodyHeight 区间就进第几页, + * 保证超长静态模板也不会被截断。 + */ +import type { AnyControl, TableControl } from '@/types/control' +import type { PageSetup, Section, TemplateData } from '@/types/template' +import { toMm } from '@/core/units' +import { isControlPrintable, resolveControlContent } from './data-binder' +import { + buildSectionControls, + computePageMetrics, + shouldRenderSection, +} from './header-footer' +import { getSharedMeasurer, type TextMeasurer } from './measure' +import { + buildTableModel, + minStartHeight, + sliceTable, + totalTableHeight, + type TableModel, +} from './table-engine' +import { isDataTable } from './table-cells' +import { + MAX_PAGES, + type EvalContext, + type LayoutPage, + type LayoutResult, + type PageMetrics, + type PlacedControl, + type PlacedNode, + type PlacedTable, + type RenderWarning, +} from './types' + +/** 几何比较容差(mm),消除序列化四舍五入带来的边界抖动 */ +const EPS = 0.5 + +export interface LayoutOptions { + /** 自定义文本测量器(默认用共享的 DOM 测量器) */ + measurer?: TextMeasurer + /** 最大页数保护 */ + maxPages?: number +} + +/* ------------------------------ 正文结构分析 ------------------------------ */ + +interface BodyPlan { + /** 驱动分页的流式表格(绑定了数据源) */ + flowTable: TableControl | null + /** 表格上方控件 → 第 1 页 */ + above: AnyControl[] + /** 与表格垂直重叠的控件 → 第 1 页原位 */ + overlap: AnyControl[] + /** 表格下方控件 → 末页,随表格高度下移 */ + below: AnyControl[] +} + +function analyzeBody(components: AnyControl[], unit: PageSetup['unit']): { + plan: BodyPlan + warnings: RenderWarning[] +} { + const warnings: RenderWarning[] = [] + const tables = components.filter( + (c): c is TableControl => c.type === 'table' && isDataTable(c), + ) + + if (tables.length > 1) { + warnings.push({ + code: 'CONTENT_OVERFLOW', + message: `正文存在 ${tables.length} 个数据表格,仅第一个(${tables[0]!.id})参与分页流动,其余按静态控件渲染`, + controlId: tables[1]!.id, + }) + } + + const flowTable = tables[0] ?? null + if (!flowTable) { + return { plan: { flowTable: null, above: components, overlap: [], below: [] }, warnings } + } + + const tTop = toMm(flowTable.top, unit) + const tBottom = tTop + toMm(flowTable.height, unit) + + const above: AnyControl[] = [] + const overlap: AnyControl[] = [] + const below: AnyControl[] = [] + + for (const c of components) { + if (c.id === flowTable.id) continue + const top = toMm(c.top, unit) + const bottom = top + toMm(c.height, unit) + if (bottom <= tTop + EPS) above.push(c) + else if (top >= tBottom - EPS) below.push(c) + else overlap.push(c) + } + + return { plan: { flowTable, above, overlap, below }, warnings } +} + +/* ------------------------------- 控件定位 ------------------------------- */ + +async function placeControls( + components: AnyControl[], + ctx: EvalContext, + unit: PageSetup['unit'], + topShift = 0, +): Promise<{ placed: PlacedControl[]; warnings: RenderWarning[] }> { + const placed: PlacedControl[] = [] + const warnings: RenderWarning[] = [] + + for (const control of components) { + if (control.type === 'zone') continue + if (!isControlPrintable(control, ctx)) continue + + // 静态表格(布局网格 / 非流动表格)在这里整体渲染 + if (control.type === 'table') continue + + const { content, warnings: w } = await resolveControlContent(control, ctx) + warnings.push(...w) + placed.push({ + kind: 'control', + id: control.id, + left: toMm(control.left, unit), + top: toMm(control.top, unit) + topShift, + width: toMm(control.width, unit), + height: toMm(control.height, unit), + angle: control.angle, + content, + control, + }) + } + + return { placed, warnings } +} + +/** 静态表格(无数据源或非首个数据表格)整表渲染,不参与分页 */ +function placeStaticTables( + components: AnyControl[], + flowTableId: string | null, + ctx: EvalContext, + unit: PageSetup['unit'], + measurer: TextMeasurer, + topShift = 0, +): { placed: PlacedTable[]; warnings: RenderWarning[] } { + const placed: PlacedTable[] = [] + const warnings: RenderWarning[] = [] + + for (const control of components) { + if (control.type !== 'table' || control.id === flowTableId) continue + if (!isControlPrintable(control, ctx)) continue + + const widthMm = toMm(control.width, unit) + const heightMm = toMm(control.height, unit) + const model = buildTableModel({ control, ctx, measurer, widthMm, heightMm }) + warnings.push(...model.warnings) + placed.push({ + kind: 'table', + id: control.id, + left: toMm(control.left, unit), + top: toMm(control.top, unit) + topShift, + width: widthMm, + height: Math.max(heightMm, totalTableHeight(model)), + control, + columns: model.columns, + columnWidths: model.columnWidths, + headerRows: model.headerRows, + rows: model.rows, + footerRows: model.footerRows, + isLastSlice: true, + }) + } + + return { placed, warnings } +} + +/* -------------------------------- 分页骨架 ------------------------------- */ + +/** 表格切片在各页上的落位结果(尚未解析上下方控件) */ +interface TableSkeleton { + /** 每页一个切片;空数组表示表格无内容 */ + slices: Array<{ table: PlacedTable; pageIndex: number }> + /** 表格实际占用的页数 */ + pageCount: number + /** 末页上表格的实际底边(mm,相对正文区顶) */ + lastBottom: number + warnings: RenderWarning[] +} + +function paginateFlowTable( + model: TableModel, + tableLeft: number, + tableWidth: number, + tableTop: number, + metrics: PageMetrics, + reserveBelow: number, + maxPages: number, +): TableSkeleton { + const warnings: RenderWarning[] = [] + const slices: TableSkeleton['slices'] = [] + const keepTogether = model.control.options?.keepTogether ?? false + + // keepTogether:首页放不下"表头 + 1 行 + 表尾"就整体下移到第 2 页(§7.4) + let firstPageIndex = 0 + let firstTop = tableTop + if (keepTogether && metrics.bodyHeight - tableTop < minStartHeight(model)) { + firstPageIndex = 1 + firstTop = 0 + } + + let start = 0 + let pageIndex = firstPageIndex + let lastBottom = firstTop + let guard = 0 + + for (;;) { + if (++guard > maxPages) { + warnings.push({ + code: 'PAGE_LIMIT_REACHED', + message: `分页超过 ${maxPages} 页保护上限,已截断`, + controlId: model.control.id, + }) + break + } + + const topOnPage = pageIndex === firstPageIndex ? firstTop : 0 + const availFull = Math.max(0, metrics.bodyHeight - topOnPage) + + // 先试"为下方控件预留空间"的窄预算;能一次放完,说明这就是末页 + let slice = null as ReturnType | null + if (reserveBelow > 0 && availFull - reserveBelow > 0) { + const trial = sliceTable(model, { avail: availFull - reserveBelow, start }) + if (trial.isLast) slice = trial + } + if (!slice) { + slice = sliceTable(model, { avail: availFull, start }) + // 用满预算才放完 → 下方控件挤不下,让它们独占下一页 + if (slice.isLast && reserveBelow > 0 && availFull - slice.height < reserveBelow) { + slice = { ...slice, isLast: false } + } + } + + warnings.push(...slice.warnings) + + // 跳过完全为空的尾片(上一页已放完全部数据行):否则会渲染出一个空表 + 多余合计行 + const isEmptySlice = + slice.rows.length === 0 && slice.headerRows.length === 0 && slice.footerRows.length === 0 + if (!isEmptySlice) { + slices.push({ + table: { + kind: 'table', + id: model.control.id, + left: tableLeft, + top: topOnPage, + width: tableWidth, + height: slice.height, + control: model.control, + columns: model.columns, + columnWidths: model.columnWidths, + headerRows: slice.headerRows, + rows: slice.rows, + footerRows: slice.footerRows, + isLastSlice: slice.isLast, + }, + pageIndex, + }) + } + + lastBottom = topOnPage + slice.height + + if (slice.isLast) break + if (slice.nextStart === start && slice.rows.length === 0) { + // 没有任何前进 → 防死循环 + warnings.push({ + code: 'CONTENT_OVERFLOW', + message: '表格可用高度不足以容纳任何一行,已终止分页', + controlId: model.control.id, + }) + break + } + + start = slice.nextStart + pageIndex++ + } + + return { + slices, + pageCount: pageIndex + 1, + lastBottom, + warnings, + } +} + +/* ================================ 主入口 ================================ */ + +export async function layout( + template: TemplateData, + data: Record, + options: LayoutOptions = {}, +): Promise { + const measurer = options.measurer ?? getSharedMeasurer() + const maxPages = options.maxPages ?? MAX_PAGES + const warnings: RenderWarning[] = [] + + const doc = template.document + const unit = doc.page.unit + const sections = doc.sections ?? [] + const headerSection = sections.find((s) => s.type === 'header') as Section | undefined + const footerSection = sections.find((s) => s.type === 'footer') as Section | undefined + const bodySection = sections.find((s) => s.type === 'body') as Section | undefined + + const metrics = computePageMetrics(doc.page, headerSection, footerSection) + const baseCtx: EvalContext = { data } + + const bodyComponents = bodySection?.components ?? [] + const { plan, warnings: planWarnings } = analyzeBody(bodyComponents, unit) + warnings.push(...planWarnings) + + /* ── 情况 A:无流式表格 → 按位置切页 ── */ + if (!plan.flowTable) { + const staticPages = await layoutStaticOnly( + plan.above, + baseCtx, + unit, + metrics, + measurer, + maxPages, + warnings, + ) + const pages = await buildPages( + staticPages.length, + new Map(staticPages.map((p, i) => [i, p])), + headerSection, + footerSection, + baseCtx, + unit, + warnings, + ) + return { pages, metrics, page: doc.page, warnings: dedupeWarnings(warnings) } + } + + /* ── 情况 B:有流式表格 ── */ + const table = plan.flowTable + const tableTop = toMm(table.top, unit) + const tableLeft = toMm(table.left, unit) + const tableWidth = toMm(table.width, unit) + const tableBottom = tableTop + toMm(table.height, unit) + + const model = buildTableModel({ + control: table, + ctx: baseCtx, + measurer, + widthMm: tableWidth, + heightMm: toMm(table.height, unit), + }) + warnings.push(...model.warnings) + + // 下方控件相对表格底边的偏移量与所需高度 + const belowOffsets = plan.below.map((c) => ({ + control: c, + delta: toMm(c.top, unit) - tableBottom, + })) + const reserveBelow = belowOffsets.reduce( + (max, b) => Math.max(max, b.delta + toMm(b.control.height, unit)), + 0, + ) + + const skeleton = paginateFlowTable( + model, + tableLeft, + tableWidth, + tableTop, + metrics, + reserveBelow, + maxPages, + ) + warnings.push(...skeleton.warnings) + + const tableLastPage = skeleton.slices.at(-1)?.pageIndex ?? 0 + // 下方控件放得下就跟在末片后面,否则独占新的一页(top 从 0 起) + const belowFitsOnTablePage = skeleton.lastBottom + reserveBelow <= metrics.bodyHeight + EPS + const belowPageIndex = belowFitsOnTablePage ? tableLastPage : tableLastPage + 1 + const belowBaseTop = belowFitsOnTablePage ? skeleton.lastBottom : 0 + + const totalPages = Math.max(1, Math.max(tableLastPage, belowPageIndex) + 1) + const lastPageNo = totalPages + + /* ── 组装每页正文 ── */ + const bodyByPage = new Map() + const pushNode = (pageIndex: number, node: PlacedNode): void => { + const list = bodyByPage.get(pageIndex) + if (list) list.push(node) + else bodyByPage.set(pageIndex, [node]) + } + + for (const { table: slice, pageIndex } of skeleton.slices) pushNode(pageIndex, slice) + + // 第 1 页:表格上方 + 重叠控件 + 静态表格 + const firstCtx: EvalContext = { ...baseCtx, page: 1, pages: totalPages } + const firstControls = await placeControls([...plan.above, ...plan.overlap], firstCtx, unit) + warnings.push(...firstControls.warnings) + for (const c of firstControls.placed) pushNode(0, c) + + const staticTables = placeStaticTables( + [...plan.above, ...plan.overlap], + table.id, + firstCtx, + unit, + measurer, + ) + warnings.push(...staticTables.warnings) + for (const t of staticTables.placed) pushNode(0, t) + + // 末页:表格下方控件,按表格实际底边下移 + if (plan.below.length > 0) { + const belowCtx: EvalContext = { ...baseCtx, page: lastPageNo, pages: totalPages } + for (const { control, delta } of belowOffsets) { + if (control.type === 'zone') continue + if (!isControlPrintable(control, belowCtx)) continue + if (control.type === 'table') continue + const { content, warnings: w } = await resolveControlContent(control, belowCtx) + warnings.push(...w) + pushNode(belowPageIndex, { + kind: 'control', + id: control.id, + left: toMm(control.left, unit), + top: belowBaseTop + delta, + width: toMm(control.width, unit), + height: toMm(control.height, unit), + angle: control.angle, + content, + control, + }) + } + + const belowStatic = placeStaticTables( + plan.below, + table.id, + { ...baseCtx, page: lastPageNo, pages: totalPages }, + unit, + measurer, + belowBaseTop - tableBottom, + ) + warnings.push(...belowStatic.warnings) + for (const t of belowStatic.placed) pushNode(belowPageIndex, t) + } + + /* ── 注入页眉页脚 ── */ + const pages = await buildPages( + totalPages, + bodyByPage, + headerSection, + footerSection, + baseCtx, + unit, + warnings, + ) + + return { pages, metrics, page: doc.page, warnings: dedupeWarnings(warnings) } +} + +/* ---------------------------- 无表格:按位置切页 ---------------------------- */ + +/** + * 无流式表格时的退化策略:控件按 top 落在第几个 bodyHeight 区间就进第几页。 + * 返回「每页的正文节点数组」,页眉页脚由调用方统一注入。 + */ +async function layoutStaticOnly( + components: AnyControl[], + baseCtx: EvalContext, + unit: PageSetup['unit'], + metrics: PageMetrics, + measurer: TextMeasurer, + maxPages: number, + warnings: RenderWarning[], +): Promise { + const bodyH = Math.max(1, metrics.bodyHeight) + + // 控件按 top 落在第几个 bodyHeight 区间就进第几页 + let maxPageIndex = 0 + const buckets = new Map() + for (const c of components) { + const idx = Math.max(0, Math.min(maxPages - 1, Math.floor(toMm(c.top, unit) / bodyH))) + maxPageIndex = Math.max(maxPageIndex, idx) + const list = buckets.get(idx) + if (list) list.push(c) + else buckets.set(idx, [c]) + } + + const totalPages = maxPageIndex + 1 + const bodyByPage = new Map() + + for (const [idx, list] of buckets) { + const ctx: EvalContext = { ...baseCtx, page: idx + 1, pages: totalPages } + const shift = -idx * bodyH + const { placed, warnings: w } = await placeControls(list, ctx, unit, shift) + warnings.push(...w) + const tables = placeStaticTables(list, null, ctx, unit, measurer, shift) + warnings.push(...tables.warnings) + bodyByPage.set(idx, [...placed, ...tables.placed]) + } + + return Array.from({ length: totalPages }, (_, i) => bodyByPage.get(i) ?? []) +} + +/* ------------------------------ 页眉页脚注入 ------------------------------ */ + +async function buildPages( + totalPages: number, + bodyByPage: Map, + headerSection: Section | undefined, + footerSection: Section | undefined, + baseCtx: EvalContext, + unit: PageSetup['unit'], + warnings: RenderWarning[], +): Promise { + const pages: LayoutPage[] = [] + + for (let i = 0; i < totalPages; i++) { + // 页码变量按页求值 —— "第 X 页 / 共 Y 页" 正确的前提 + const pageCtx: EvalContext = { ...baseCtx, page: i + 1, pages: totalPages } + + let header: PlacedControl[] = [] + if (shouldRenderSection(headerSection, 'header', i, totalPages)) { + const r = await buildSectionControls(headerSection?.components, pageCtx, unit) + warnings.push(...r.warnings) + header = r.controls + } + + let footer: PlacedControl[] = [] + if (shouldRenderSection(footerSection, 'footer', i, totalPages)) { + const r = await buildSectionControls(footerSection?.components, pageCtx, unit) + warnings.push(...r.warnings) + footer = r.controls + } + + pages.push({ index: i, pageNo: i + 1, header, body: bodyByPage.get(i) ?? [], footer }) + } + + return pages +} + +/* -------------------------------- 工具 -------------------------------- */ + +/** 同一条告警可能在多页重复产生(页眉每页解析一次),这里按 code+message+controlId 去重 */ +function dedupeWarnings(list: RenderWarning[]): RenderWarning[] { + const seen = new Set() + const out: RenderWarning[] = [] + for (const w of list) { + const key = `${w.code}|${w.controlId ?? ''}|${w.message}` + if (seen.has(key)) continue + seen.add(key) + out.push(w) + } + return out +} diff --git a/openprint/src/core/layout-engine/table-cells.spec.ts b/openprint/src/core/layout-engine/table-cells.spec.ts new file mode 100644 index 0000000..f282526 --- /dev/null +++ b/openprint/src/core/layout-engine/table-cells.spec.ts @@ -0,0 +1,422 @@ +/** + * 设计期单元格网格模型单测(table-cells.ts) + * + * 覆盖:有/无 cells 的回落、buildDesignGrid 行语义、ensureCells 物化、 + * patchCellText(表头/静态/数据样例行语义)、patchCellStyle(合并/清除)、 + * setCellSpan(横向合并 + 吞格清空)、resolveCellStyle 优先级、normalizeCellRows、designRowInfo。 + */ +import { describe, expect, it } from 'vitest' +import type { TableCell, TableControl } from '@/types/control' +import { + addTableColumn, + buildDesignGrid, + computeSpanLayout, + designRowInfo, + ensureCells, + moveTableColumn, + normalizeCellRows, + patchCell, + patchCellStyle, + patchCellText, + removeTableColumn, + removeTableRow, + resolveCellStyle, + setCellRowSpan, + setCellSpan, + setGridRows, +} from './table-cells' + +function baseTable(over: Partial = {}): TableControl { + return { + id: 't1', + type: 'table', + left: 0, + top: 0, + width: 180, + height: 60, + columns: [ + { title: '序号', expression: '{{rowIndex + 1}}', width: 15, align: 'center', headerAlign: 'center' }, + { title: '名称', field: 'name', width: 60 }, + { title: '数量', field: 'qty', width: 25, align: 'right', headerAlign: 'center' }, + ], + options: { borders: 'all', verticalAlign: 'middle' }, + ...over, + } +} + +describe('buildDesignGrid', () => { + it('数据表无 cells:由列配置瞬时推导 表头+数据样例行+静态尾行', () => { + const g = buildDesignGrid(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(g.isData).toBe(true) + expect(g.headerRows).toBe(1) + expect(g.rowCount).toBe(1 + 1 + 1) // header + data template + 1 static + expect(g.colCount).toBe(3) + // 表头第 1 行:文本=列标题、加粗、对齐跟随 headerAlign + expect(g.cells[0]![0]!.text).toBe('序号') + expect(g.cells[0]![0]!.style?.bold).toBe(true) + expect(g.cells[0]![0]!.style?.align).toBe('center') + // 数据样例行第 2 行:带 field(来自列配置的 field) + expect(g.cells[1]![1]!.field).toBe('name') + expect(g.cells[1]![2]!.field).toBe('qty') + // 静态尾行:空 + expect(g.cells[2]![0]).toEqual({}) + }) + + it('布局网格(无 dataSource)无 cells:行数取 designRows(含表头)', () => { + const g = buildDesignGrid(baseTable({ designRows: 4 })) + expect(g.isData).toBe(false) + expect(g.headerRows).toBe(1) // 列有标题 → 自动补 1 行表头 + expect(g.rowCount).toBe(5) // 1 表头 + 4 正文 + expect(g.colCount).toBe(3) + expect(g.cells.every((r) => r.length === 3)).toBe(true) + }) + + it('已有 cells:按语义行数归一化(列数补齐到当前列数)', () => { + const cells = [ + [{ text: 'A' }, { text: 'B' }, { text: 'C' }, { text: '多余列应被丢弃' }], + [{ text: 'x' }, { text: 'y' }, { text: 'z' }], + ] + const g = buildDesignGrid(baseTable({ dataSource: 'items', cells, staticRows: 0 })) + expect(g.rowCount).toBe(2) // header(1) + data(1) + 0 + expect(g.colCount).toBe(3) + expect(g.cells[0]![3]).toBeUndefined() + expect(g.cells[0]![0]!.text).toBe('A') + }) + + it('列数为 0 时兜底为 1 列,不崩溃', () => { + const g = buildDesignGrid(baseTable({ columns: [] })) + expect(g.colCount).toBe(1) + expect(g.rowCount).toBeGreaterThan(0) + }) +}) + +describe('ensureCells', () => { + it('无 cells 时物化出完整网格并带上语义行数', () => { + const next = ensureCells(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(next.cells).toBeDefined() + expect(next.cells!.length).toBe(3) + expect(next.headerRows).toBe(1) + expect(next.staticRows).toBe(1) + expect(next.designRows).toBe(0) + }) + + it('已存在且尺寸匹配则返回结构(不重建,保持既有内容)', () => { + const cells = [ + [{ text: 'H1' }, { text: 'H2' }, { text: 'H3' }], + [{ field: 'a' }, { field: 'b' }, { field: 'c' }], + ] + const orig = baseTable({ dataSource: 'items', cells, staticRows: 0 }) + const next = ensureCells(orig) + expect(next.cells).toBe(cells) + expect(next.headerRows).toBe(1) + }) +}) + +describe('patchCellText 行语义', () => { + it('表头/静态行:写 text 字面量', () => { + const t = baseTable({ dataSource: 'items', staticRows: 1 }) + const next = patchCellText(t, 0, 0, '序号X') // 表头行 + expect(next.cells![0]![0]!.text).toBe('序号X') + }) + + it('静态尾行:写 text 字面量', () => { + const t = baseTable({ dataSource: 'items', staticRows: 1 }) + const next = patchCellText(t, 2, 0, '合计') // 静态行 + expect(next.cells![2]![0]!.text).toBe('合计') + }) + + it('数据样例行:与当前占位符一致 → 原样返回(不固化占位符)', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 1, '{{item.name}}') + expect(next).toBe(t) + }) + + it('数据样例行:纯字段引用回写为 field', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 1, '{{item.amount}}') + expect(next.cells![1]![1]!.field).toBe('amount') + expect(next.cells![1]![1]!.expression).toBeUndefined() + }) + + it('数据样例行:含表达式 → 写 expression 并清 field', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 2, '{{row.price * row.qty}}') + expect(next.cells![1]![2]!.expression).toBe('{{row.price * row.qty}}') + expect(next.cells![1]![2]!.field).toBeUndefined() + }) + + it('数据样例行:清空 → 同时清 expression 与 field', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 1, '') + expect(next.cells![1]![1]!.expression).toBeUndefined() + expect(next.cells![1]![1]!.field).toBeUndefined() + }) +}) + +describe('patchCellStyle / resolveCellStyle', () => { + it('resolveCellStyle 优先级:单元格 > 列 > 表格默认', () => { + const t = baseTable({ + options: { defaultCellStyle: { align: 'left', color: '#000' } }, + columns: [{ title: 'A', width: 10, style: { align: 'center', color: '#f00' } }], + }) + const resolved = resolveCellStyle(t, t.columns[0], { style: { align: 'right', bold: true } }) + expect(resolved.align).toBe('right') // 单元格覆盖 + expect(resolved.color).toBe('#f00') // 列兜底 + expect(resolved.bold).toBe(true) + }) + + it('patchCellStyle 合并并覆盖传入键;undefined 表示清除', () => { + let t = baseTable({ dataSource: 'items' }) + t = patchCellStyle(t, 0, 0, { bold: true, color: '#ff0000' }) + expect(t.cells![0]![0]!.style?.bold).toBe(true) + expect(t.cells![0]![0]!.style?.color).toBe('#ff0000') + t = patchCellStyle(t, 0, 0, { color: undefined }) + expect(t.cells![0]![0]!.style?.bold).toBe(true) + expect(t.cells![0]![0]!.style?.color).toBeUndefined() + }) +}) + +describe('setCellSpan 横向合并', () => { + it('本格 colSpan=n,被吞格清空(保留矩阵规整)', () => { + const t = baseTable({ dataSource: 'items' }) + const next = setCellSpan(t, 1, 0, 2) // 数据样例行第0列合并2列 + expect(next.cells![1]![0]!.colSpan).toBe(2) + expect(next.cells![1]![1]!.text).toBeUndefined() + expect(next.cells![1]![1]!.field).toBeUndefined() + expect(next.cells![1]![1]!.colSpan).toBeUndefined() + }) + + it('合并数越界自动夹紧到剩余列数', () => { + const t = baseTable({ dataSource: 'items' }) + const next = setCellSpan(t, 1, 1, 99) // 剩 2 列(c=1,2) + expect(next.cells![1]![1]!.colSpan).toBe(2) + }) +}) + +describe('designRowInfo', () => { + it('数据表行语义正确', () => { + const g = buildDesignGrid(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(designRowInfo(g, 0).kind).toBe('header') + expect(designRowInfo(g, 1).kind).toBe('data') + expect(designRowInfo(g, 1).isDataTemplate).toBe(true) + expect(designRowInfo(g, 2).kind).toBe('static') + }) + + it('布局网格:第 0 行为表头,其余为 static', () => { + const g = buildDesignGrid(baseTable({ designRows: 3 })) + expect(designRowInfo(g, 0).kind).toBe('header') + expect(designRowInfo(g, 1).kind).toBe('static') + expect(designRowInfo(g, 2).kind).toBe('static') + }) +}) + +describe('normalizeCellRows', () => { + it('行/列不足补空、超出截断', () => { + const out = normalizeCellRows([[{ text: 'a' }], [{ text: 'b' }, { text: 'c' }]], 3, 2) + expect(out.length).toBe(3) + expect(out[0]!.length).toBe(2) + expect(out[0]![0]!.text).toBe('a') + expect(out[0]![1]).toEqual({}) + expect(out[1]![1]!.text).toBe('c') + }) +}) + +describe('patchCell 不可变更新', () => { + it('返回新控件且只改目标格', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCell(t, 0, 0, { text: '改' }) + expect(next).not.toBe(t) + expect(next.cells![0]![0]!.text).toBe('改') + expect(t.cells).toBeUndefined() + }) +}) + +describe('网格结构变更(#100)', () => { + it('setGridRows 布局网格增正文行:保留已有内容并按目标维度补空', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + t.cells![0]![0] = { text: 'H' } + t.cells![1]![1] = { text: 'B' } + const next = setGridRows(t, { designRows: 4 }) + expect(next.headerRows).toBe(1) + expect(next.designRows).toBe(4) + expect(next.cells!.length).toBe(5) // 1 表头 + 4 正文 + expect(next.cells![0]![0]!.text).toBe('H') // 表头保留 + expect(next.cells![1]![1]!.text).toBe('B') // 正文保留 + expect(next.cells![4]![0]).toEqual({}) // 新补空行 + }) + + it('setGridRows 数据表增静态尾行', () => { + const t = ensureCells(baseTable({ dataSource: 'items', staticRows: 0 })) + const next = setGridRows(t, { staticRows: 2 }) + expect(next.staticRows).toBe(2) + expect(next.cells!.length).toBe(4) // 1 表头 + 1 数据样例 + 2 尾 + expect(Boolean(next.dataSource?.trim())).toBe(true) + }) + + it('setGridRows 改表头行数:新增表头行用列标题兜底', () => { + const t = baseTable({ designRows: 2 }) + const next = setGridRows(t, { headerRows: 2 }) + expect(next.headerRows).toBe(2) + expect(next.cells!.length).toBe(4) // 2 表头 + 2 正文 + expect(next.cells![1]![0]!.text).toBe('序号') // 新表头行取列标题 + }) + + it('setGridRows 减行数:截断尾部(保留前面内容)', () => { + const t = ensureCells(baseTable({ designRows: 5 })) + t.cells![0]![0] = { text: 'H' } + const next = setGridRows(t, { designRows: 2 }) + expect(next.cells!.length).toBe(3) // 1 表头 + 2 正文 + expect(next.cells![0]![0]!.text).toBe('H') + }) + + it('addTableColumn:列与每行单元格同步增加,新列表头默认填标题', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + const next = addTableColumn(t, { title: '新列', width: 20 }) + expect(next.columns.length).toBe(4) + expect(next.cells!.every((r) => r.length === 4)).toBe(true) + expect(next.cells![0]![3]!.text).toBe('新列') // 表头新列取标题 + }) + + it('removeTableColumn:删除指定列(含对应单元格),至少留 1 列', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + t.cells![0]![1] = { text: 'B-col1' } + const next = removeTableColumn(t, 1) + expect(next.columns.length).toBe(2) + expect(next.cells!.every((r) => r.length === 2)).toBe(true) + expect(next.columns[0]!.title).toBe('序号') + expect(next.columns[1]!.title).toBe('数量') // 原第 2 列(数量)左移 + expect(next.cells![0]![1]!.text).toBe('数量') // 单元格同步左移 + }) + + it('removeTableColumn:最后一列不可删', () => { + const t = baseTable({ columns: [{ title: 'only', width: 10 }] }) + const next = removeTableColumn(t, 0) + expect(next.columns.length).toBe(1) + }) + + it('removeTableRow:删除指定行(含对应单元格),至少留 1 行', () => { + const t = ensureCells(baseTable({ designRows: 3 })) + t.cells![2]![0] = { text: '第三行' } + const next = removeTableRow(t, 2) + const grid = buildDesignGrid(next) + expect(grid.rowCount).toBe(3) // 4 行 - 1 + expect(grid.designRows).toBe(2) + expect(grid.cells.some((r) => r[0]?.text === '第三行')).toBe(false) + expect(grid.cells[1]![0]?.text).toBeUndefined() // 第 2 行未受影响 + }) + + it('removeTableRow:删表头行时 headerRows 减一', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + const next = removeTableRow(t, 0) + expect(buildDesignGrid(next).headerRows).toBe(0) + expect(buildDesignGrid(next).rowCount).toBe(2) + }) + + it('removeTableRow:数据表的数据样例行(唯一 body 行)不可删', () => { + const t = ensureCells(baseTable({ dataSource: 'items', staticRows: 1 })) + const next = removeTableRow(t, 1) // 第 1 行是数据样例行(headerRows=1) + expect(next).toBe(t) // 不变 + }) + + it('removeTableRow:单行表格不可删', () => { + const t = ensureCells(baseTable({ height: 8, headerRows: 0, designRows: 0, columns: [{ title: '', width: 10 }] })) + expect(buildDesignGrid(t).rowCount).toBe(1) + const next = removeTableRow(t, 0) + expect(next).toBe(t) + }) + + it('moveTableColumn:左右搬动列与单元格', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + const next = moveTableColumn(t, 0, 2) + expect(next.columns[2]!.title).toBe('序号') + expect(next.cells![0]![2]!.text).toBe('序号') // 单元格同列位置搬动 + expect(next.cells![0]![0]!.text).toBe('名称') + }) +}) + +describe('computeSpanLayout 合并布局', () => { + // 构造 cells 矩阵的便捷函数:rows 是「每行每列的对象字面量」二维数组 + const mk = (rows: Array>>): TableCell[][] => + rows.map((r) => r.map((c) => ({ ...c }))) + + it('无合并:全部 skip=false、跨度=1', () => { + const cells = mk([ + [{}, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout.every((r) => r.every((s) => !s.skip && s.colSpan === 1 && s.rowSpan === 1))).toBe(true) + }) + + it('横向合并:锚点 colSpan=2,右侧被吞', () => { + const cells = mk([ + [{ colSpan: 2 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.colSpan).toBe(2) + expect(layout[0]![0]!.skip).toBe(false) + expect(layout[0]![1]!.skip).toBe(true) // 被横向吞掉 + expect(layout[1]![1]!.skip).toBe(false) // 下一行不受同行跨列影响 + }) + + it('纵向合并:锚点 rowSpan=2,下方同列被吞', () => { + const cells = mk([ + [{ rowSpan: 2 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.rowSpan).toBe(2) + expect(layout[1]![0]!.skip).toBe(true) // 被纵向吞掉 + expect(layout[0]![1]!.skip).toBe(false) + expect(layout[1]![1]!.skip).toBe(false) + }) + + it('跨列 + 跨行组合:2×2 块,其余三格均被吞', () => { + const cells = mk([ + [{ colSpan: 2, rowSpan: 2 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.colSpan).toBe(2) + expect(layout[0]![0]!.rowSpan).toBe(2) + expect(layout[0]![1]!.skip).toBe(true) + expect(layout[1]![0]!.skip).toBe(true) + expect(layout[1]![1]!.skip).toBe(true) + }) + + it('越界收敛:rowSpan 超出网格底边被截断', () => { + const cells = mk([ + [{ rowSpan: 9 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.rowSpan).toBe(2) // 只剩 2 行,截断为 2 + expect(layout[1]![0]!.skip).toBe(true) + }) +}) + +describe('setCellRowSpan 纵向合并', () => { + it('rowSpan=2:本格写 rowSpan,下方同列清空', () => { + const t = ensureCells(baseTable({ designRows: 3 })) + const next = setCellRowSpan(t, 0, 0, 2) + expect(next.cells![0]![0]!.rowSpan).toBe(2) + expect(next.cells![1]![0]!.text).toBeUndefined() + expect(next.cells![1]![0]!.field).toBeUndefined() + expect(next.cells![2]![0]!.rowSpan).toBeUndefined() // 只吞 2 行 + }) + + it('rowSpan=1:清除合并', () => { + const t = ensureCells(baseTable({ designRows: 3 })) + const merged = setCellRowSpan(t, 0, 0, 2) + const cleared = setCellRowSpan(merged, 0, 0, 1) + expect(cleared.cells![0]![0]!.rowSpan).toBeUndefined() + }) + + it('越界 rowSpan 收敛到网格底边', () => { + const t = ensureCells(baseTable({ designRows: 2 })) // 共 1 表头 + 2 正文 = 3 行 + const next = setCellRowSpan(t, 0, 0, 99) + expect(next.cells![0]![0]!.rowSpan).toBe(3) // 从第 0 行到底边共 3 行 + expect(next.cells![2]![0]!.rowSpan).toBeUndefined() + }) +}) diff --git a/openprint/src/core/layout-engine/table-cells.ts b/openprint/src/core/layout-engine/table-cells.ts new file mode 100644 index 0000000..f9567c7 --- /dev/null +++ b/openprint/src/core/layout-engine/table-cells.ts @@ -0,0 +1,728 @@ +/** + * table-cells —— 表格设计期单元格网格模型(纯函数,可测) + * + * 把"列驱动 + 数据源"的传统表格,叠加一层**设计期可编辑的单元格网格**, + * 使得双击任意单元格即可填写文字 / 绑定字段 / 调字体 / 填色, + * 同时**完全兼容**未设置 cells 的旧模板(回落到列驱动渲染)。 + * + * 行语义(designRowInfo): + * - 数据表(有 dataSource):前 headerRows 行为表头;第 headerRows 行为"数据样例行"(模板, + * 编辑它 = 编辑该列所有数据行);其后 staticRows 行为静态尾行(合计 / 备注)。 + * 可视行与 cells 行 1:1 对应(cells 长度 = headerRows + 1 + staticRows)。 + * - 布局网格(无 dataSource):前 headerRows 行为表头(由列标题推导),其后 designRows 行为 + * 静态内容(cells 长度 = headerRows + designRows)。 + * + * ⚠️ `designRows` 语义 = **正文行数(不含表头)**;可视行总数一律取 `grid.rowCount`。 + */ +import type { TableCell, TableCellStyle, TableColumn, TableControl } from '@/types/control' +import { isAggToken, parseAggToken, stripItems } from './aggregate' + +export const DEFAULT_HEADER_ROWS = 1 + +export type DesignRowKind = 'header' | 'data' | 'static' + +/** + * 单个单元格的合并布局结果(由 computeSpanLayout 统一计算)。 + * - skip=true:该格被左/上某个合并格吞掉,渲染时跳过(矩阵仍留位保持规整)。 + * - colSpan / rowSpan:本格实际生效的合并跨度(已收敛到网格边界)。 + */ +export interface CellSpan { + skip: boolean + colSpan: number + rowSpan: number +} + +export interface DesignRowInfo { + kind: DesignRowKind + /** 在 control.cells 中的行下标(数据表与可视行 1:1) */ + cellRow: number + /** 是否为数据表的数据样例行(编辑影响整列) */ + isDataTemplate: boolean +} + +export interface DesignGrid { + isData: boolean + headerRows: number + staticRows: number + /** 布局网格的正文行数(不含表头);数据表恒为 0 */ + designRows: number + /** 可视行总数(含表头 / 数据样例行 / 静态尾行) */ + rowCount: number + colCount: number + cells: TableCell[][] +} + +/** 该表格是否数据表(绑定了数据源数组,或自带内嵌 data 行) */ +export function isDataTable(control: TableControl): boolean { + return ( + Boolean(control.dataSource?.trim()) || + (Array.isArray(control.data) && control.data.length > 0) + ) +} + +/** + * 表头行数。 + * 未显式设置时:**有任一列标题 → 1 行,否则 0 行**(数据表 / 布局网格同一规则), + * 与运行期旧版 `columns.some(c => c.title)` 判定一致,保证老模板打印结果不变。 + */ +export function headerRowsOf(control: TableControl): number { + if (typeof control.headerRows === 'number') return Math.max(0, control.headerRows) + return (control.columns ?? []).some((c) => c.title) ? DEFAULT_HEADER_ROWS : 0 +} + +export function staticRowsOf(control: TableControl): number { + return isDataTable(control) ? Math.max(0, control.staticRows ?? 0) : 0 +} + +function emptyRow(colCount: number): TableCell[] { + return Array.from({ length: colCount }, () => ({})) +} + +/** 从列配置推导单元格(用于无 cells 时的瞬时渲染 / 首次初始化) */ +function cellFromColumn(col: TableColumn, role: 'header' | 'data'): TableCell { + if (role === 'header') { + return { + text: col.title, + style: { + backgroundColor: col.headerBackgroundColor, + // 与运行期 table-engine 表头默认一致:headerAlign → align → center + align: col.headerAlign ?? col.align ?? 'center', + bold: true, + }, + } + } + return { + field: col.field, + expression: col.expression, + style: { + backgroundColor: col.cellBackgroundColor, + align: col.align, + ...(col.style ?? {}), + }, + } +} + +/** + * 布局网格的正文行数(不含表头)。 + * 真理源优先级:已物化的 cells 行数 > 显式 designRows > 按控件高度推算。 + * (以 cells 为先,才不会在改了表头行数后把用户已填的行挤掉。) + */ +function layoutBodyRows(control: TableControl, headerRows: number): number { + if (control.cells && control.cells.length > 0) { + return Math.max(1, control.cells.length - headerRows) + } + if (typeof control.designRows === 'number' && control.designRows > 0) return control.designRows + const rowH = Math.max(4, control.options?.rowHeight ?? 8) + const bodyH = Math.max(0, control.height - headerRows * rowH) + return Math.max(1, Math.floor(bodyH / rowH) || 1) +} + +/** 该单元格是否"从未被写过内容"(用于表头兜底判定;显式清空写入的 `text: ''` 不算空) */ +function isBlankCell(cell: TableCell | undefined): boolean { + return !cell || (cell.text === undefined && !cell.field && !cell.expression) +} + +/** + * 计算设计期网格(不修改入参)。 + * - 若 control.cells 存在:归一化其行数 / 列数后使用;整行空白的表头行用列标题兜底 + * (用户改了列标题却看不到,比"尊重空表头"更反直觉)。 + * - 否则:由列配置瞬时推导。 + * - 数据表 = headerRows 行表头 + 1 行数据样例 + staticRows 行静态尾行 + * - 布局网格 = headerRows 行表头 + designRows 行静态正文 + */ +export function buildDesignGrid(control: TableControl): DesignGrid { + const cols = control.columns ?? [] + const colCount = Math.max(1, cols.length) + const data = isDataTable(control) + const headerRows = headerRowsOf(control) + const staticRows = staticRowsOf(control) + const bodyRows = data ? 1 : layoutBodyRows(control, headerRows) + const rowCount = headerRows + bodyRows + staticRows + + // 由列配置推导的"标准网格",同时充当已有 cells 的兜底来源 + const derived = normalizeCellRows( + [ + ...Array.from({ length: headerRows }, () => cols.map((c) => cellFromColumn(c, 'header'))), + ...(data + ? [cols.map((c) => cellFromColumn(c, 'data'))] + : Array.from({ length: bodyRows }, () => emptyRow(colCount))), + ...Array.from({ length: staticRows }, () => emptyRow(colCount)), + ], + rowCount, + colCount, + ) + + let cells = derived + if (control.cells && control.cells.length > 0) { + cells = normalizeCellRows(control.cells, rowCount, colCount) + for (let r = 0; r < headerRows; r++) { + if (cells[r]!.every(isBlankCell)) cells[r] = derived[r]! + } + } + + return { + isData: data, + headerRows, + staticRows, + designRows: data ? 0 : bodyRows, + rowCount, + colCount, + cells, + } +} + +/** + * 设计期各可视行的行高(mm)—— **设计画布与运行期布局网格共用**,是"所见即所得"的根: + * 表头按 8mm/行 分配(最多占 32% 高度),剩余高度由正文行均分,合计恰好 = 控件高度。 + * + * @param heightMm 控件高度(mm)。协议单位非 mm 时由调用方换算后传入。 + */ +export function designRowHeights(control: TableControl, heightMm?: number): number[] { + const grid = buildDesignGrid(control) + const totalH = Math.max(1, heightMm ?? control.height) + const headerRows = grid.headerRows + const bodyCount = Math.max(1, grid.rowCount - headerRows) + const headerShare = Math.min(totalH * 0.32, headerRows * 8) + const headerRowH = headerRows > 0 ? headerShare / headerRows : 0 + const bodyH = Math.max(4, (totalH - headerShare) / bodyCount) + const out: number[] = [] + for (let r = 0; r < grid.rowCount; r++) out.push(r < headerRows ? headerRowH : bodyH) + return out +} + +/** 把任意 cells 矩阵归一化为 rowCount × colCount(不足补空、超出截断) */ +export function normalizeCellRows( + cells: TableCell[][], + rowCount: number, + colCount: number, +): TableCell[][] { + const out: TableCell[][] = [] + for (let r = 0; r < rowCount; r++) { + const src = cells[r] ?? [] + const row: TableCell[] = [] + for (let c = 0; c < colCount; c++) { + row.push(src[c] ? { ...src[c] } : {}) + } + out.push(row) + } + return out +} + +/** 给定可视行号,返回其语义(表头 / 数据样例行 / 静态尾行) */ +export function designRowInfo(grid: DesignGrid, r: number): DesignRowInfo { + if (!grid.isData) { + return { kind: r < grid.headerRows ? 'header' : 'static', cellRow: r, isDataTemplate: false } + } + if (r < grid.headerRows) return { kind: 'header', cellRow: r, isDataTemplate: false } + if (r === grid.headerRows) return { kind: 'data', cellRow: r, isDataTemplate: true } + return { kind: 'static', cellRow: r, isDataTemplate: false } +} + +/** + * 设计期行角色名(双击编辑时显示在首列左侧,帮助用户识别当前所在行)。 + * 聚合尾行按单元格 token 判定:大写金额 / 总计 / 本页合计。 + */ +export function rowRoleLabel(grid: DesignGrid, r: number): string { + const info = designRowInfo(grid, r) + if (info.kind === 'header') return grid.headerRows > 1 ? `标题行 ${r + 1}` : '标题行' + if (info.isDataTemplate) return '数据行' + const row = grid.cells[r] ?? [] + const tokens = row.map((c) => parseAggToken(c.text)) + if (tokens.some((t) => t === 'pageCap' || t === 'totalCap')) return '大写金额行' + if (tokens.some((t) => t === 'totalSum' || t === 'totalAvg' || t === 'totalCount')) return '总计行' + if (tokens.some((t) => t === 'pageSum' || t === 'pageAvg' || t === 'pageCount')) return '本页合计行' + // 其余非表头、非聚合 token 的行(数据样例行 / 备注 / 签字栏等)统一叫"数据行" + return '数据行' +} + +/** + * 物化 cells 到控件(首次双击编辑或列数变化时调用)。 + * 返回带完整 cells 的新控件;若已存在且长度匹配则原样返回。 + */ +export function ensureCells(control: TableControl): TableControl { + const grid = buildDesignGrid(control) + const headerRows = grid.headerRows + const staticRows = grid.staticRows + const designRows = grid.designRows + const existing = control.cells + const matches = + existing && + existing.length === grid.rowCount && + existing.every((row) => row.length === grid.colCount) + if (matches) { + return { ...control, headerRows, staticRows, designRows } + } + return { + ...control, + headerRows, + staticRows, + designRows, + cells: grid.cells, + } +} + +/** 浅合并单元格样式:later 覆盖 former 的已定义字段 */ +export function mergeCellStyle(...parts: Array): TableCellStyle { + const out: TableCellStyle = {} + for (const p of parts) { + if (!p) continue + for (const [k, v] of Object.entries(p)) { + if (v !== undefined && v !== null) (out as Record)[k] = v + } + } + return out +} + +/** 取某单元格最终样式(表格默认 → 列样式 → 单元格样式) */ +export function resolveCellStyle( + control: TableControl, + column: TableColumn | undefined, + cell: TableCell | undefined, +): TableCellStyle { + return mergeCellStyle(control.options?.defaultCellStyle, column?.style, cell?.style) +} + +/** + * 按行语义取单元格最终样式。 + * 表头行**不吃 `column.style`** —— 列样式是给数据单元格用的,套到表头上会让 + * "把某列数据调成红色"顺带把标题也染红,与直觉相悖。设计期与运行期共用本函数。 + */ +export function resolveCellStyleFor( + control: TableControl, + column: TableColumn | undefined, + cell: TableCell | undefined, + kind: DesignRowKind, +): TableCellStyle { + return resolveCellStyle(control, kind === 'header' ? undefined : column, cell) +} + +/** 不可变更新某单元格(合并 patch),返回新控件 */ +export function patchCell( + control: TableControl, + r: number, + c: number, + patch: Partial, +): TableControl { + const grid = buildDesignGrid(control) + const next = grid.cells.map((row, ri) => + ri === r ? row.map((cell, ci) => (ci === c ? { ...cell, ...patch } : cell)) : row, + ) + return { + ...control, + cells: next, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/* ----------------------------- 网格结构变更 ----------------------------- */ + +/** + * 设定表格的语义行数并物化 cells(保留已有内容,按需补空 / 截断)。 + * 同时回写 headerRows / designRows / staticRows 元数据,使与设计期网格 1:1 对齐。 + * + * 参数按控件类型生效: + * - 布局网格(无 dataSource):designRows = 正文行数;staticRows 恒为 0。 + * - 数据表:固定 1 行数据样例行(模板),staticRows = 静态尾行(备注 / 签字栏)。 + * 未传的维度沿用当前值,便于只改某一维。 + * + * ⚠️ 不能走 ensureCells:它按"cells 长度优先推导 designRows",会无视显式 designRows。 + * 这里以"目标维度"为准直接归一 cells。 + */ +export function setGridRows( + control: TableControl, + rows: { headerRows?: number; designRows?: number; staticRows?: number }, +): TableControl { + const headerRows = Math.max(0, rows.headerRows ?? headerRowsOf(control)) + const isData = isDataTable(control) + const staticRows = isData ? Math.max(0, rows.staticRows ?? staticRowsOf(control)) : 0 + const designRows = isData ? 0 : Math.max(0, rows.designRows ?? layoutBodyRows(control, headerRows)) + const colCount = Math.max(1, control.columns?.length ?? 0) + const rowCount = headerRows + (isData ? 1 : designRows) + staticRows + + // 已有 cells → 按目标维度直接归一(保留内容);无 cells → 先按新语义推导再取 + const base = + control.cells && control.cells.length > 0 + ? control.cells + : buildDesignGrid({ ...control, headerRows, designRows, staticRows }).cells + const cells = normalizeCellRows(base, rowCount, colCount) + return { ...control, headerRows, designRows, staticRows, cells } +} + +/** 在列尾追加一列(并同步扩展每行的单元格);返回新控件 */ +export function addTableColumn(control: TableControl, col?: Partial): TableControl { + const idx = control.columns?.length ?? 0 + const newCol: TableColumn = { + title: col?.title ?? `列${idx + 1}`, + field: col?.field, + expression: col?.expression, + width: col?.width ?? 30, + align: col?.align, + headerAlign: col?.headerAlign, + cellBackgroundColor: col?.cellBackgroundColor, + headerBackgroundColor: col?.headerBackgroundColor, + cellPadding: col?.cellPadding, + style: col?.style, + } + const grid = buildDesignGrid(control) // 当前规范矩阵 + const cells = grid.cells.map((row, r) => { + const row2 = [...row, {}] + // 新列表头单元格默认填入列标题(与设计期"列标题推导表头"一致) + if (r < grid.headerRows && newCol.title) row2[row2.length - 1] = { text: newCol.title } + return row2 + }) + return { + ...control, + columns: [...(control.columns ?? []), newCol], + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/** 删除指定列(并同步裁剪每行的对应单元格);至少保留 1 列 */ +export function removeTableColumn(control: TableControl, index: number): TableControl { + const cols = control.columns ?? [] + if (cols.length <= 1 || index < 0 || index >= cols.length) return control + const grid = buildDesignGrid(control) // 当前规范矩阵 + const cells = grid.cells.map((row) => row.filter((_, i) => i !== index)) + return { + ...control, + columns: cols.filter((_, i) => i !== index), + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/** 删除指定行(同步裁剪该行单元格);至少保留 1 行 */ +export function removeTableRow(control: TableControl, index: number): TableControl { + const grid = buildDesignGrid(control) + if (index < 0 || index >= grid.rowCount || grid.rowCount <= 1) return control + // 数据表的"数据样例行"(唯一 body 行)不可删除,否则破坏逐行渲染模型 + if (grid.isData && index === grid.headerRows) return control + const cells = grid.cells.filter((_, r) => r !== index) + let { headerRows, staticRows, designRows } = grid + if (index < headerRows) headerRows -= 1 + else if (grid.isData) staticRows = Math.max(0, staticRows - 1) + else designRows = Math.max(0, designRows - 1) + return { ...control, headerRows, staticRows, designRows, cells } +} + +/** 列左右移动(上移 / 下移到相邻位置);同时搬动每行的对应单元格 */ +export function moveTableColumn(control: TableControl, from: number, to: number): TableControl { + const cols = control.columns ?? [] + if (from < 0 || from >= cols.length || to < 0 || to >= cols.length || from === to) return control + const grid = buildDesignGrid(control) // 当前规范矩阵 + const cells = grid.cells.map((row) => { + const r = [...row] + const [cell] = r.splice(from, 1) + r.splice(to, 0, cell!) + return r + }) + const movedCols = [...cols] + const [col] = movedCols.splice(from, 1) + movedCols.splice(to, 0, col!) + return { + ...control, + columns: movedCols, + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/** + * 单元格文本写回(把"用户在 DOM 里敲的字"翻译成协议语义): + * - 表头 / 静态行:写 `text`(纯字面量) + * - 数据样例行:写 `expression`(模板串——含 `{{}}` 则运行期插值,纯文字则原样输出), + * 并清空 `field`,避免 field/expression 双写时语义打架 + * + * 若文本与"当前占位符"完全一致,视为未改动,直接返回原控件(避免把占位符固化成表达式)。 + */ +export function patchCellText( + control: TableControl, + r: number, + c: number, + raw: string, +): TableControl { + const grid = buildDesignGrid(control) + const info = designRowInfo(grid, r) + const text = raw.replace(/\u00a0/g, ' ').trim() + const cell = grid.cells[r]?.[c] ?? {} + const col = control.columns[c] + + if (info.isDataTemplate) { + // 占位符显示与 table-design-render 的 fieldPlaceholder 严格对齐,确保"未改动即不写回" + const fieldPh = (f: string): string => (f.includes('[]') ? `{{${f}}}` : `{{item.${f}}}`) + const current = cell.expression + ?? (cell.field ? fieldPh(cell.field) : col?.field ? fieldPh(col.field) : '') + if (text === current) return control + if (!text) return patchCell(control, r, c, { expression: undefined, field: undefined }) + // "{{item.xxx}}" 单一普通字段引用 → 回写为 field,保持模型最简 + const single = /^\{\{\s*item\.([A-Za-z0-9_$.]+)\s*\}\}$/.exec(text) + if (single) return patchCell(control, r, c, { field: single[1], expression: undefined }) + // "{{items[].xxx}}" 含数组标记的单一字段引用 → 同样回写为 field(运行期 resolveBinding 会去前缀取值) + const singleArr = /^\{\{\s*([A-Za-z0-9_$\u4e00-\u9fa5][A-Za-z0-9_$\u4e00-\u9fa5.\[\]]*\[\]\.[A-Za-z0-9_$\u4e00-\u9fa5.\[\]]*)\s*\}\}$/.exec(text) + if (singleArr) return patchCell(control, r, c, { field: singleArr[1], expression: undefined }) + return patchCell(control, r, c, { expression: text, field: undefined }) + } + + if ((cell.text ?? '') === text) return control + return patchCell(control, r, c, { text }) +} + +/** 不可变合并某单元格样式(只覆盖传入的键,undefined 表示"清除该项") */ +export function patchCellStyle( + control: TableControl, + r: number, + c: number, + style: TableCellStyle, +): TableControl { + const grid = buildDesignGrid(control) + const current = grid.cells[r]?.[c]?.style ?? {} + const merged: TableCellStyle = { ...current } + for (const [k, v] of Object.entries(style)) { + if (v === undefined) delete (merged as Record)[k] + else (merged as Record)[k] = v + } + return patchCell(control, r, c, { style: merged }) +} + +/** + * 设置单元格横向合并跨度。 + * 合并 = 本格 colSpan=n,被吞掉的右侧格清空(渲染时按 span 跳过,模型仍留位保证矩阵规整)。 + */ +export function setCellSpan(control: TableControl, r: number, c: number, span: number): TableControl { + const grid = buildDesignGrid(control) + const max = grid.colCount - c + const n = Math.max(1, Math.min(Math.floor(span), max)) + let next = patchCell(control, r, c, { colSpan: n > 1 ? n : undefined }) + for (let i = 1; i < n; i++) { + next = patchCell(next, r, c + i, { text: undefined, field: undefined, expression: undefined, colSpan: undefined }) + } + return next +} + +/** + * 设置单元格纵向合并跨度。 + * 合并 = 本格 rowSpan=m,被吞掉的下方同列格清空(渲染时按 span 跳过,模型仍留位保持矩阵规整)。 + * 超界自动收敛到网格底边,故改行数 / 删行不会产生非法合并。 + */ +export function setCellRowSpan(control: TableControl, r: number, c: number, span: number): TableControl { + const grid = buildDesignGrid(control) + const max = grid.rowCount - r + const n = Math.max(1, Math.min(Math.floor(span), max)) + let next = patchCell(control, r, c, { rowSpan: n > 1 ? n : undefined }) + for (let k = 1; k < n; k++) { + next = patchCell(next, r + k, c, { + text: undefined, + field: undefined, + expression: undefined, + colSpan: undefined, + rowSpan: undefined, + }) + } + return next +} + +/** + * 统一计算整张网格的合并布局(colSpan + rowSpan 一起算),供设计 overlay 与运行期引擎共用。 + * + * 规则: + * - 跨列:本格 colSpan=n → 同行右侧 n-1 格被跳过(同一行内)。 + * - 跨行:本格 rowSpan=m → 下方 m-1 行同列(及所跨各列)被跳过。 + * - 两种跨度可同时作用于同一锚点格(形成 2×N / N×2 / N×M 的合并块)。 + * - 所有跨度超界自动收敛到网格边界,因此改行数 / 删列 / 移动列都不会产生非法合并或丢列。 + * + * 返回与 cells 同形的矩阵:skip=true 的格由相邻锚点合并,渲染端据此决定"是否输出该 td"。 + */ +export function computeSpanLayout( + cells: TableCell[][], + rowCount: number, + colCount: number, +): CellSpan[][] { + const out: CellSpan[][] = Array.from({ length: rowCount }, () => + Array.from({ length: colCount }, () => ({ skip: false, colSpan: 1, rowSpan: 1 }) as CellSpan), + ) + // carry[c] = 进入当前行时,第 c 列仍被上方某个 rowspan 占用的剩余行数(>0 表示该格需跳过) + const carry = new Array(colCount).fill(0) + for (let r = 0; r < rowCount; r++) { + const curCarry = carry.slice() + const nextCarry = carry.map((v) => Math.max(0, v - 1)) + let c = 0 + while (c < colCount) { + if (curCarry[c]! > 0) { + out[r]![c]!.skip = true + c++ + continue + } + const cell = cells[r]?.[c] ?? {} + const cs = Math.max(1, Math.min(Math.floor(cell.colSpan ?? 1), colCount - c)) + const rs = Math.max(1, Math.min(Math.floor(cell.rowSpan ?? 1), rowCount - r)) + out[r]![c]!.colSpan = cs + out[r]![c]!.rowSpan = rs + if (cs > 1) { + // 同一行被横向吞掉的格,标记 skip(矩阵自我完备:skip=true 即"不作为独立 td 输出") + for (let k = 1; k < cs; k++) out[r]![c + k]!.skip = true + } + if (rs > 1) { + // 被跨的每一列在后续行都被占用(含跨列覆盖到的列) + for (let k = 0; k < cs; k++) nextCarry[c + k] = Math.max(nextCarry[c + k]!, rs - 1) + } + c += cs + } + carry.length = 0 + carry.push(...nextCarry) + } + return out +} + +/** 不可变更新某列(数据表数据样例行编辑 = 编辑整列) */ +export function patchColumnStyle( + control: TableControl, + c: number, + style: TableCellStyle, +): TableControl { + const cols = control.columns.map((col, ci) => + ci === c ? { ...col, style: { ...(col.style ?? {}), ...style } } : col, + ) + return { ...control, columns: cols } +} + +/* ----------------------------- 行 / 列插入 ----------------------------- */ + +/** + * 在 `atRow` 处插入一空行(原行下移)。 + * - 布局网格:直接插入正文区,designRows+1。 + * - 数据表:数据样例行固定在 headerRows,任何插入都落在静态尾行区(避免破坏"单数据样例行"不变量), + * 故 atRow 被收敛到 headerRows+1 之后;staticRows+1。 + */ +export function insertTableRow(control: TableControl, atRow: number): TableControl { + const grid = buildDesignGrid(control) + const colCount = grid.colCount + const cells = grid.cells.map((row) => row.map((c) => ({ ...c }))) + const newRow: TableCell[] = Array.from({ length: colCount }, () => ({})) + let insertAt = atRow + let headerRows = grid.headerRows + let designRows = grid.designRows + let staticRows = grid.staticRows + if (grid.isData) { + const minStatic = grid.headerRows + 1 + insertAt = Math.max(minStatic, atRow) + staticRows += 1 + } else { + designRows += 1 + } + cells.splice(insertAt, 0, newRow) + return { ...control, headerRows, designRows, staticRows, cells } +} + +/** 在 `atCol` 处插入一空列(原列右移),同步扩展表头与每行的单元格 */ +export function insertTableColumn(control: TableControl, atCol: number): TableControl { + const grid = buildDesignGrid(control) + const cells = grid.cells.map((row) => { + const r = [...row] + r.splice(atCol, 0, {}) + return r + }) + const idx = (control.columns?.length ?? 0) + 1 + const newCol: TableColumn = { + title: `列${idx}`, + width: 30, + align: 'left', + headerAlign: 'center', + } + const cols = [...(control.columns ?? [])] + cols.splice(atCol, 0, newCol) + return { + ...control, + columns: cols, + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/* ----------------------------- 默认尾行结构 ----------------------------- */ + +export interface SeedTailOptions { + /** 参与合计(数值)的列索引;不传则从数据首行采样 / 列 aggregate 标记推断 */ + numericColumns?: number[] + /** 大写金额取哪列的总计(主金额列索引);不传取最后一个数值列 */ + moneyColumn?: number + /** 是否植入"大写金额"行(默认 true)。初始化默认表格可传 false,只留本页合计 + 总计两行 */ + capital?: boolean +} + +/** + * 植入"本页合计 / 总计 / 大写金额"三行尾结构(学习金蝶等 ERP 单据设计): + * - 数值列(数量 / 单价 / 金额…)尾格写入聚合 token:本页合计=`{{#pageSum}}`、总计=`{{#totalSum}}`、大写金额=`{{#totalCap}}`; + * - 第一列写入行名标签(本页合计 / 总计 / 大写金额),让用户一眼识别; + * - 非数值列(名称 / 备注等字符串)尾格留空,不参与计算; + * - 数值列自动标记 `aggregate:true`,参与尾行聚合。 + * + * 幂等:若已存在任意聚合 token 尾行则不重复植入。 + */ +export function seedSummaryTail(control: TableControl, opts: SeedTailOptions = {}): TableControl { + const grid = buildDesignGrid(control) + // 已植入尾行则不重复:识别聚合 token 或首列的尾行标签(单列表格 token 落在首列,故需同时识别标签) + const tailLabels = new Set(['本页合计', '总计', '大写金额']) + const seeded = grid.cells.slice(grid.headerRows + 1).some( + (row) => + row.some((c) => isAggToken(c.text)) || + (row[0] != null && tailLabels.has(row[0]!.text ?? '')), + ) + if (seeded) return control + const cols = control.columns ?? [] + const colCount = grid.colCount + + // 数值列判定 + const numeric = new Set(opts.numericColumns ?? []) + if (opts.numericColumns === undefined) { + const sample = Array.isArray(control.data) && control.data.length ? (control.data[0] as Record) : undefined + cols.forEach((c, i) => { + const key = stripItems(c.field) + if (c.aggregate === true || c.aggregate === 'sum' || c.aggregate === 'avg' || c.aggregate === 'count') { + numeric.add(i) + } else if (key && sample && typeof sample[key] === 'number') { + numeric.add(i) + } + }) + } + const money = opts.moneyColumn ?? [...numeric].sort((a, b) => b - a)[0] ?? -1 + + const makeRow = (label: string, token: string | null): TableCell[] => + Array.from({ length: colCount }, (_, c) => { + if (c === 0) return { text: label, style: { bold: true, align: 'left' } } + if (token && numeric.has(c)) return { text: token, style: { bold: true, align: 'right' } } + return {} + }) + const pageRow = makeRow('本页合计', '{{#pageSum}}') + const totalRow = makeRow('总计', '{{#totalSum}}') + const withCap = opts.capital ?? true + const capRow: TableCell[] = Array.from({ length: colCount }, (_, c) => { + if (c === 0) return { text: '大写金额', style: { bold: true, align: 'left' } } + if (c === money) return { text: '{{#totalCap}}', style: { bold: true, align: 'left' } } + return {} + }) + + // 数值列标记 aggregate:true + const newCols = cols.map((c, i) => (numeric.has(i) ? { ...c, aggregate: true as const } : c)) + + const tailRows = withCap ? [pageRow, totalRow, capRow] : [pageRow, totalRow] + const cells = [...grid.cells, ...tailRows] + return { + ...control, + columns: newCols, + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows + tailRows.length, + designRows: grid.designRows, + } +} diff --git a/openprint/src/core/layout-engine/table-engine.spec.ts b/openprint/src/core/layout-engine/table-engine.spec.ts new file mode 100644 index 0000000..0b9c4a4 --- /dev/null +++ b/openprint/src/core/layout-engine/table-engine.spec.ts @@ -0,0 +1,497 @@ +import { describe, expect, it } from 'vitest' + +import { + buildTableModel, + normalizeColumnWidths, + sliceTable, +} from '@/core/layout-engine/table-engine' +import { isDataTable } from '@/core/layout-engine/table-cells' +import type { TableColumn, TableControl } from '@/types/control' +import type { EvalContext } from './types' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +type Model = ReturnType + +describe('normalizeColumnWidths —— 列宽归一化', () => { + it('按比例缩放使列宽和 = totalWidth', () => { + const cols: TableColumn[] = [ + { title: 'A', field: 'a', width: 10 }, + { title: 'B', field: 'b', width: 20 }, + ] + const out = normalizeColumnWidths(cols, 30) + expect(out).toEqual([10, 20]) + expect(out.reduce((s, w) => s + w, 0)).toBeCloseTo(30, 6) + }) + + it('全部为 0 时均分', () => { + const cols: TableColumn[] = [{ title: 'A', field: 'a', width: 0 }, { title: 'B', field: 'b', width: 0 }] + const out = normalizeColumnWidths(cols, 10) + expect(out[0]).toBeCloseTo(5, 6) + expect(out[1]).toBeCloseTo(5, 6) + }) + + it('含 0 宽列时保持 0,其余按比例', () => { + const cols: TableColumn[] = [ + { title: 'A', field: 'a', width: 2 }, + { title: 'B', field: 'b', width: 0 }, + { title: 'C', field: 'c', width: 2 }, + ] + const out = normalizeColumnWidths(cols, 8) + expect(out[0]).toBeCloseTo(4, 6) + expect(out[1]).toBe(0) + expect(out[2]).toBeCloseTo(4, 6) + expect(out.reduce((s, w) => s + w, 0)).toBeCloseTo(8, 6) + }) +}) + +describe('buildTableModel —— 表格建模', () => { + const measurer = createCjkMeasurer() + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: { repeatHeader: true }, + } as unknown as Parameters[0]['control'] + + it('根据 dataSource 生成与数据等量的数据行', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }, { name: 'C', qty: 3 }] } } + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 50 }) + expect(model.rows.length).toBe(3) + expect(model.columnWidths.length).toBe(2) + expect(model.columnWidths.reduce((s, w) => s + w, 0)).toBeCloseTo(100, 6) + expect(model.headerRows.length).toBeGreaterThan(0) + expect(model.isLayoutGrid).toBe(false) + }) + + it('无数据源时退化为布局网格(固定行数)', () => { + const grid = { ...control, dataSource: '' } as unknown as Parameters[0]['control'] + const model = buildTableModel({ control: grid, ctx: { data: {} }, measurer, widthMm: 100, heightMm: 50 }) + expect(model.isLayoutGrid).toBe(true) + expect(model.rows.length).toBeGreaterThan(0) + }) + + /** 取出第一条数据行的第 col 列渲染文本 */ + function dataCellText(ctrl: Parameters[0]['control'], ctx: EvalContext, col: number): string { + const model = buildTableModel({ control: ctrl, ctx, measurer, widthMm: 100, heightMm: 50 }) + const row = model.rows.find((r) => r.kind === 'data') + if (!row) throw new Error('no data row') + return (row.cells[col] as { text: string }).text + } + + it('列格式 decimal:数字按小数位+千分位渲染', () => { + const c = { + ...control, + columns: [{ field: 'price', title: '单价', width: 40, align: 'right', format: { kind: 'decimal', digits: 2 } }], + } as unknown as Parameters[0]['control'] + expect(dataCellText(c, { data: { items: [{ price: 1234.5 }] } }, 0)).toBe('1,234.50') + }) + + it('列格式 currency:带币种符号', () => { + const c = { + ...control, + columns: [{ field: 'amount', title: '金额', width: 40, align: 'right', format: { kind: 'currency', code: 'CNY' } }], + } as unknown as Parameters[0]['control'] + expect(dataCellText(c, { data: { items: [{ amount: 9999.9 }] } }, 0)).toBe('¥9,999.90') + }) + + it('单元格 format 覆盖列 format', () => { + const c = { + ...control, + columns: [{ field: 'qty', title: '数量', width: 40, align: 'right', format: { kind: 'decimal', digits: 2 } }], + cells: [ + [{ text: '数量' }], + [{ field: 'qty', format: { kind: 'int' } }], + ], + } as unknown as Parameters[0]['control'] + expect(dataCellText(c, { data: { items: [{ qty: 1234.5 }] } }, 0)).toBe('1,235') + }) + + it('表头/静态行绑定 cell.field 应解析主表标量(回归:staticCellText 曾忽略 field)', () => { + const c = { + ...control, + dataSource: '', + headerRows: 1, + staticRows: 1, + cells: [ + [{ field: 'order.no' }, { text: '数量' }], + [{ field: 'order.total' }, { text: '' }], + ], + } as unknown as Parameters[0]['control'] + const ctx = { data: { order: { no: 'NO-001', total: 5 } } } + const model = buildTableModel({ control: c, ctx, measurer, widthMm: 100, heightMm: 50 }) + // 表头单元格:绑定 order.no → 解析出主表标量(修复前为空) + expect(model.headerRows[0]!.cells[0]!.text).toBe('NO-001') + // 静态尾行单元格:绑定 order.total → 解析出 5 + const staticRow = model.rows.find((r) => r.kind === 'static') + expect(staticRow).toBeTruthy() + expect(staticRow!.cells[0]!.text).toBe('5') + }) +}) + +describe('sliceTable —— 分页切片(含空尾片回归)', () => { + function mkModel(rowCount: number): Model { + return { + control: { id: 't', type: 'table', options: { repeatHeader: true, repeatFooter: false } } as Model['control'], + columns: [], + columnWidths: [], + headerRows: [{ kind: 'header', height: 8, cells: [] }], + rows: Array.from({ length: rowCount }, (_, i) => ({ + kind: 'data', + height: 8, + dataIndex: i, + cells: [], + })) as Model['rows'], + footerRows: [{ kind: 'summary', height: 8, cells: [{ text: '合计', align: 'left' }] }], + dataRows: [], + warnings: [], + isLayoutGrid: false, + } as Model + } + + it('从 start=0 切片:取全部行,末片挂合计行', () => { + const model = mkModel(3) + const slice = sliceTable(model, { avail: 100, start: 0 }) + expect(slice.rows.length).toBe(3) + expect(slice.footerRows.length).toBeGreaterThan(0) // 末片 + 空间充足 → 合计行 + expect(slice.isLast).toBe(true) + expect(slice.nextStart).toBe(3) + }) + + it('空尾片(start 越界)不挂合计行,也不产出多余数据行', () => { + const model = mkModel(3) + const slice = sliceTable(model, { avail: 100, start: 3 }) + expect(slice.rows.length).toBe(0) // 关键回归:不再渲染第二个空表 + expect(slice.footerRows.length).toBe(0) // 关键回归:空尾片不再挂合计 + expect(slice.isLast).toBe(true) + expect(slice.nextStart).toBe(3) + }) + + it('空间不足时本页少放,nextStart 后移', () => { + const model = mkModel(5) + // 每页能放 2 行(header 8 + 2*8 = 24 ≤ avail 24;第 3 行 8 会使 32 > 24) + const slice = sliceTable(model, { avail: 24, start: 0 }) + expect(slice.rows.length).toBe(2) + expect(slice.nextStart).toBe(2) + expect(slice.isLast).toBe(false) + }) +}) + +/* ----------------------- 合计行(options.summaryRow) ----------------------- */ + +function mkSummaryControl(opts: TableControl['options']): Parameters[0]['control'] { + return { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: opts, + } as unknown as Parameters[0]['control'] +} + +describe('buildTableModel —— 合计行(options.summaryRow)', () => { + const measurer = createCjkMeasurer() + + it('求和:表尾合计行在聚合列显示总和,首列显示标签', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }, { name: 'C', qty: 3 }] } } + const model = buildTableModel({ + control: mkSummaryControl({ summaryRow: { type: 'sum', fields: ['qty'], label: '合计' } }), + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + expect(model.footerRows.length).toBe(1) + const row = model.footerRows[0]! + expect(row.kind).toBe('summary') + expect(row.cells[0]!.text).toBe('合计') + const qtyCell = row.cells.find((c) => c.text === '6') + expect(qtyCell).toBeTruthy() + expect(qtyCell!.bold).toBe(true) + expect(qtyCell!.align).toBe('right') + }) + + it('计数:聚合列显示数据总行数', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }, { name: 'C', qty: 3 }] } } + const model = buildTableModel({ + control: mkSummaryControl({ summaryRow: { type: 'count', fields: ['qty'], label: '合计' } }), + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + const row = model.footerRows[0]! + expect(row.cells.find((c) => c.text === '3')).toBeTruthy() + }) + + it('未配置合计行时不生成表尾', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }] } } + const model = buildTableModel({ + control: mkSummaryControl({}), + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + expect(model.footerRows.length).toBe(0) + }) +}) + +describe('buildTableModel —— 分组小计(groupBy + summaryRow)', () => { + const measurer = createCjkMeasurer() + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + groupBy: 'cat', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: { summaryRow: { type: 'sum', fields: ['qty'], label: '合计' } }, + } as unknown as Parameters[0]['control'] + + it('按分组字段生成组头 + 每组小计 + 末尾总计', () => { + const ctx = { + data: { + items: [ + { name: 'A', qty: 1, cat: 'x' }, + { name: 'B', qty: 2, cat: 'x' }, + { name: 'C', qty: 3, cat: 'y' }, + ], + }, + } + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 50 }) + expect(model.rows.some((r) => r.kind === 'group')).toBe(true) + const subs = model.rows.filter((r) => r.kind === 'subtotal') + expect(subs.length).toBe(2) + // 每组内求和:x=1+2=3,y=3,均显示 3 + expect(subs.every((s) => s.cells.find((c) => c.text === '3'))).toBe(true) + // 末尾总计 = 1+2+3 = 6 + const total = model.footerRows.find((r) => r.kind === 'summary') + expect(total?.cells.find((c) => c.text === '6')).toBeTruthy() + }) + + it('分组小计应用自定义标签与样式', () => { + const ctx = { + data: { + items: [ + { name: 'A', qty: 1, cat: 'x' }, + { name: 'B', qty: 2, cat: 'x' }, + ], + }, + } + const model = buildTableModel({ + control: { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + groupBy: 'cat', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: { + summaryRow: { + type: 'sum', + fields: ['qty'], + label: '总计', + subtotalLabel: '${key} 分组小计', + subtotalStyle: { bold: true, backgroundColor: '#EEF3FF', color: '#c00' }, + }, + }, + } as unknown as Parameters[0]['control'], + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + const subs = model.rows.filter((r) => r.kind === 'subtotal') + expect(subs.length).toBe(1) + expect(subs[0]!.cells[0]!.text).toBe('x 分组小计') + // 整行套用 subtotalStyle(标签格也应带颜色) + expect(subs[0]!.cells[0]!.color).toBe('#c00') + expect(subs[0]!.cells[0]!.background).toBe('#EEF3FF') + // 聚合列显示本组求和 1+2=3 + expect(subs[0]!.cells.find((c) => c.text === '3')).toBeTruthy() + }) + + it('自定义合计表达式在表尾生效(footer 显示求值结果)', () => { + const ctx = { data: { items: [{ name: 'A', amount: 10 }, { name: 'B', amount: 20 }] } } + const model = buildTableModel({ + control: { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + // 首列为非聚合列(放标签),第二列为聚合列(放自定义求值结果) + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'amount', title: '金额', width: 40, align: 'right' }, + ], + options: { + summaryRow: { + type: 'custom', + fields: ['amount'], + label: '自定义合计', + expression: 'sum.amount + 5', + }, + }, + } as unknown as Parameters[0]['control'], + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + expect(model.footerRows.length).toBe(1) + const row = model.footerRows[0]! + expect(row.kind).toBe('summary') + expect(row.cells[0]!.text).toBe('自定义合计') + // 聚合列显示 (10+20) + 5 = 35 + expect(row.cells.find((c) => c.text === '35')).toBeTruthy() + }) +}) + +describe('rowSpan 纵向合并渲染', () => { + const measurer = createCjkMeasurer() + + it('布局网格:纵向合并时下方被吞单元格不进 cells,锚点带 rowSpan', () => { + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 40, + columns: [ + { title: 'A', width: 50 }, + { title: 'B', width: 50 }, + ], + headerRows: 0, + designRows: 2, + cells: [ + [ + { rowSpan: 2, text: '合并' }, + { text: '右1' }, + ], + [ + { text: '下左' }, + { text: '下右' }, + ], + ], + options: {}, + } as unknown as TableControl + + const model = buildTableModel({ control, ctx: { data: {} }, measurer, widthMm: 100, heightMm: 40 }) + expect(model.isLayoutGrid).toBe(true) + // 第 0 行:锚点(合并, rowSpan=2) + 右1 → 2 个单元格 + expect(model.rows[0]!.cells.length).toBe(2) + expect(model.rows[0]!.cells[0]!.rowSpan).toBe(2) + // 第 1 行:被吞的"下左"不出现,只剩"下右" → 1 个单元格 + expect(model.rows[1]!.cells.length).toBe(1) + expect(model.rows[1]!.cells[0]!.text).toBe('下右') + }) + + it('数据行模板的 rowSpan 被强制为 1(不跨记录)', () => { + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 60, + dataSource: 'items', + columns: [ + { field: 'name', title: '名称', width: 50 }, + { field: 'qty', title: '数量', width: 50 }, + ], + cells: [ + [{ text: '名称' }, { text: '数量' }], + [{ rowSpan: 2, field: 'name' }, { field: 'qty' }], + ], + options: {}, + } as unknown as TableControl + + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }] } } + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 60 }) + // 每条数据行的首格不应带 rowSpan(避免跨记录合并) + expect(model.rows.every((r) => r.cells[0]!.rowSpan === undefined)).toBe(true) + }) +}) + +describe('buildTableModel —— 内嵌 data(与 dataSource 字段解耦)', () => { + const measurer = createCjkMeasurer() + + it('无 dataSource、带 data 时按内嵌数据生成数据行并解析字段', () => { + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + // 注意:这里刻意不设置 dataSource + columns: [ + { title: '名称', field: 'items[].name', width: 50 }, + { title: '数量', field: 'items[].qty', width: 50 }, + ], + headerRows: 1, + data: [ + { name: 'A', qty: 1 }, + { name: 'B', qty: 2 }, + ], + options: { pageRows: 'auto', repeatHeader: true, borders: 'all' }, + } as unknown as TableControl + + // 全局 ctx 里没有任何 items,证明数据完全来自 control.data + const ctx = { data: {} } as unknown as EvalContext + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 50 }) + expect(model.rows).toHaveLength(2) + expect(model.rows[0]!.cells[0]!.text).toBe('A') + expect(model.rows[1]!.cells[1]!.text).toBe('2') + // 表头来自 columns.title + expect(model.headerRows[0]!.cells[0]!.text).toBe('名称') + }) + + it('isDataTable 对内嵌 data 返回 true(供 analyzeBody 识别为分页流表)', () => { + const withData = { + id: 't', + type: 'table', + columns: [], + data: [{ a: 1 }], + } as unknown as TableControl + expect(isDataTable(withData)).toBe(true) + const empty = { + id: 't2', + type: 'table', + columns: [], + data: [], + } as unknown as TableControl + expect(isDataTable(empty)).toBe(false) + }) +}) diff --git a/openprint/src/core/layout-engine/table-engine.ts b/openprint/src/core/layout-engine/table-engine.ts new file mode 100644 index 0000000..f223917 --- /dev/null +++ b/openprint/src/core/layout-engine/table-engine.ts @@ -0,0 +1,839 @@ +/** + * TableEngine —— 动态表格建模与切片分页 + * 真理源:《OpenPrint-设计方案.md》§5.4(表格统一设计)、§7.4(难点清单) + * + * 覆盖 §7.4 难点: + * | 难点 | 实现位置 | + * |---|---| + * | 动态表格分页 / 表头重复 | sliceTable() 的 repeatHeader 分支 | + * | 每页行数控制 pageRows | sliceTable() 的 forcedRows | + * | 行内文本测量 rowHeightMode:'auto' | measureRowHeight() | + * | keepTogether 起始不足整体下移 | minStartHeight()(由 pagination-engine 调用) | + * | 合计/表尾 repeatFooter | sliceTable() 的 footer 预算 | + * | 空行 / 空表 skipEmptyRows | buildTableModel() | + * | 多单合并 mergeSheets | resolveRows() | + * + * 分两步:**建模**(一次性算完所有行与高度)→ **切片**(按页可用高度取行)。 + * 这样分页只是纯几何切割,不再触碰数据语义,逻辑可测且不会走样。 + * + * ## 内容真理源 = 设计期单元格网格(方案 A) + * + * 建模不再直接读列配置,而是统一经 `buildDesignGrid(control)`: + * 表头行、数据样例行(模板)、静态尾行都来自同一份 `cells` 网格 —— 设计画布画的是它、 + * 打印吃的也是它,字体/颜色/对齐/合并因此天然一致(真·所见即所得)。 + * 未设置 `cells` 的老模板由 `buildDesignGrid` 从列配置瞬时推导,行为与旧版逐字节等价。 + */ +import type { HAlign, TableCell, TableCellStyle, TableColumn, TableControl } from '@/types/control' +import { FILTERS, interpolate, resolveBinding, stringifyValue, formatCellValue } from './expression' +import { + aggValueForRows, + columnAggregatable, + formatAggNumber, + isAggToken, + parseAggToken, + stripItems, + toChineseCapitalRMB, + type FooterKind, +} from './aggregate' +import { + buildDesignGrid, + computeSpanLayout, + designRowHeights, + isDataTable, + resolveCellStyleFor, + type CellSpan, + type DesignRowKind, +} from './table-cells' +import { + buildSummaryPlan, + isEmptyRow, + planRows, + readField, + summaryLabel, + type RowPlan, +} from './group-engine' +import type { TextMeasurer } from './measure' +import type { EvalContext, RenderCell, RenderRow, RenderWarning } from './types' + +/* ------------------------------- 排版常数 ------------------------------- */ + +/** 表格文字字号(pt)—— 与设计画布 PrintTable 的 9pt 保持一致,确保"所见即所得" */ +export const TABLE_FONT_SIZE = 9 +/** + * 单元格水平内边距(mm)。 + * 说明:协议注释写的默认 4mm 若同时用作垂直内边距会让行高凭空多出 8mm, + * 对 ERP 密集表格不可用;这里把 cellPadding 解释为**水平内缩**(业界报表工具通用语义), + * 垂直方向由 CELL_PADDING_Y 单独控制。 + */ +export const DEFAULT_CELL_PADDING = 2 +/** 单元格垂直内边距(mm,上下各一份) */ +export const CELL_PADDING_Y = 1.2 +/** 最小行高(mm),防止空行被压成一条线 */ +export const MIN_ROW_HEIGHT = 6 +/** 表头行相对数据行的高度系数 */ +export const HEADER_ROW_FACTOR = 1 + +/* -------------------------------- 模型 -------------------------------- */ + +export interface TableModel { + control: TableControl + columns: TableColumn[] + /** 归一化后的各列实际宽度(mm,合计 = 控件宽度) */ + columnWidths: number[] + /** 表头行(支持多行;无表头时为空数组) */ + headerRows: RenderRow[] + /** 数据 + 组头 + 小计行(按打印顺序) */ + rows: RenderRow[] + /** 表尾块:聚合尾行(本页合计/总计/大写金额)+ 设计期静态尾行(备注 / 签字栏) */ + footerRows: RenderRow[] + /** 解析后的全表数据行(供切片期重算合计;尾行聚合的真理源) */ + dataRows: Array> + warnings: RenderWarning[] + /** 是否为布局网格(无 dataSource 的空白表格,§5.4) */ + isLayoutGrid: boolean +} + +/** 行高累加(表头 / 表尾块) */ +export function sumRowHeights(rows: RenderRow[]): number { + return rows.reduce((s, r) => s + r.height, 0) +} + +/* ------------------------------ 数据行解析 ------------------------------ */ + +function asRecord(v: unknown): Record | null { + return v !== null && typeof v === 'object' && !Array.isArray(v) + ? (v as Record) + : null +} + +/** + * 取出表格的数据行。 + * mergeSheets(§9.2.1a 多单合并):dataSource 解析出的若干数组会被**扁平拼接**成一条连续行流, + * 使多张单据一次连续打印,而不是各自起新页。 + */ +function resolveRows( + control: TableControl, + ctx: EvalContext, + warnings: RenderWarning[], +): Array> { + const path = control.dataSource?.trim() + if (path) { + const raw = resolveBinding(path, ctx) + if (raw === null || raw === undefined) { + warnings.push({ + code: 'DATASOURCE_EMPTY', + message: `表格数据源 "${path}" 在数据中不存在`, + controlId: control.id, + }) + return [] + } + + if (!Array.isArray(raw)) { + warnings.push({ + code: 'DATASOURCE_NOT_ARRAY', + message: `表格数据源 "${path}" 不是数组(实际为 ${typeof raw})`, + controlId: control.id, + }) + return [] + } + + const merge = control.options?.mergeSheets ?? false + const out: Array> = [] + for (const item of raw) { + if (Array.isArray(item)) { + // 多单合并:嵌套数组扁平化;未开启合并时忽略嵌套数组(避免打出乱序内容) + if (merge) { + for (const sub of item) { + const rec = asRecord(sub) + if (rec) out.push(rec) + } + } + continue + } + const rec = asRecord(item) + if (rec) out.push(rec) + } + + if (out.length === 0) { + warnings.push({ + code: 'DATASOURCE_EMPTY', + message: `表格数据源 "${path}" 为空数组`, + controlId: control.id, + }) + } + return out + } + + // 表格自带内嵌数据(导入数据场景):与 dataSource 字段解耦,直接作为数据行 + if (Array.isArray(control.data) && control.data.length > 0) { + const out: Array> = [] + for (const item of control.data) { + const rec = asRecord(item) + if (rec) out.push(rec) + } + return out + } + + return [] +} + +/* ------------------------------- 列宽归一化 ------------------------------ */ + +/** 把列的相对宽度按控件实际宽度等比归一,保证列宽之和 === 表格宽度 */ +export function normalizeColumnWidths(columns: TableColumn[], totalWidth: number): number[] { + const sum = columns.reduce((s, c) => s + (c.width > 0 ? c.width : 0), 0) + if (sum <= 0) { + const even = totalWidth / Math.max(1, columns.length) + return columns.map(() => even) + } + return columns.map((c) => ((c.width > 0 ? c.width : 0) / sum) * totalWidth) +} + +/* ------------------------------- 单元格取值 ------------------------------ */ + +function interp(src: string, ctx: EvalContext, errors: string[]): string { + const r = interpolate(src, ctx) + errors.push(...r.errors) + return r.text +} + +/** + * 数据行单元格取值。优先级:单元格 expression > 单元格 field > 单元格固定文字 > 列 expression > 列 field。 + * (单元格未设置任何内容时回落到列配置,既兼容老模板,也让"新增列"立刻可用。) + */ +function dataCellText( + cell: TableCell, + col: TableColumn | undefined, + ctx: EvalContext, + errors: string[], +): string { + if (cell.expression) return interp(cell.expression, ctx, errors) + if (cell.field) return formatCellValue(resolveBinding(cell.field, ctx), cell.format ?? col?.format) + if (cell.text !== undefined) return interp(cell.text, ctx, errors) + if (col?.expression) return interp(col.expression, ctx, errors) + if (col?.field) return formatCellValue(resolveBinding(col.field, ctx), col?.format) + return '' +} + +/** + * 表头 / 静态行取值。优先级与数据行对齐:单元格 expression > 单元格 field > 单元格字面量 > 列 field。 + * - 支持 {{}} 插值(如"合计:{{order.total}}"、"第 {{page}} 页"); + * - 也支持字段绑定(order.no):经 resolveBinding 解析主表标量字段; + * - 无任何标记的字面量文字原样输出(即"普通文本"约定)。 + */ +function staticCellText( + cell: TableCell, + col: TableColumn | undefined, + fallback: string, + ctx: EvalContext, + errors: string[], +): string { + if (cell.expression) return interp(cell.expression, ctx, errors) + if (cell.field) return formatCellValue(resolveBinding(cell.field, ctx), cell.format ?? col?.format) + if (cell.text !== undefined) return interp(cell.text, ctx, errors) + if (col?.field) return formatCellValue(resolveBinding(col.field, ctx), col?.format) + if (!fallback) return '' + return interp(fallback, ctx, errors) +} + +/** 聚合值显示:整数不带小数,小数保留两位,统一加千分位 */ +function formatAggregate(v: number): string { + return Number.isInteger(v) ? FILTERS.number!(v, 0) : FILTERS.number!(v, 2) +} + +/* ---------------------------- 单元格 → 渲染单元格 --------------------------- */ + +/** 与 cells[] 一一对应的几何(含 colSpan 合并后的宽度),供行高测量使用 */ +interface RowGeometry { + widths: number[] + pads: number[] +} + +interface BuiltRow { + cells: RenderCell[] + geo: RowGeometry +} + +/** 把设计期样式盖到渲染单元格上(只覆盖显式设置过的项,未设置的保留 kind 默认值) */ +function applyCellStyle( + base: RenderCell, + control: TableControl, + col: TableColumn | undefined, + cell: TableCell, + kind: DesignRowKind, +): RenderCell { + const s = resolveCellStyleFor(control, col, cell, kind) + const out: RenderCell = { ...base } + if (s.align) out.align = s.align + if (s.backgroundColor) out.background = s.backgroundColor + if (s.bold !== undefined) out.bold = s.bold + if (s.fontSize !== undefined) out.fontSize = s.fontSize + if (s.fontFamily) out.fontFamily = s.fontFamily + if (s.italic !== undefined) out.italic = s.italic + if (s.underline !== undefined) out.underline = s.underline + if (s.color) out.color = s.color + if (s.valign) out.valign = s.valign + if (s.diagonal) out.diagonal = s.diagonal + return out +} + +/** + * 摊平一行设计期单元格:处理 colSpan / rowSpan(合并宽度、跳过被吞掉的格), + * 同时产出与渲染单元格对齐的宽度 / 内边距数组,让行高测量吃到真实可用宽度。 + * + * @param spanRow 本行各列的合并布局(computeSpanLayout 已算好):skip=true 的格被上方/左侧 + * 合并吞掉,这里直接跳过;colSpan/rowSpan 作为本格的有效跨度。 + */ +function layoutDesignRow( + rowCells: TableCell[], + spanRow: CellSpan[], + columns: TableColumn[], + columnWidths: number[], + colCount: number, + make: (cell: TableCell, col: TableColumn | undefined, colIndex: number) => RenderCell, +): BuiltRow { + const cells: RenderCell[] = [] + const widths: number[] = [] + const pads: number[] = [] + let c = 0 + while (c < colCount) { + if (spanRow[c]?.skip) { + c++ + continue + } + const cell = rowCells[c] ?? {} + const span = Math.max(1, spanRow[c]!.colSpan) + const col = columns[c] + let w = 0 + for (let k = 0; k < span; k++) w += columnWidths[c + k] ?? 0 + const rc = make(cell, col, c) + if (span > 1) rc.colSpan = span + if (spanRow[c]!.rowSpan > 1) rc.rowSpan = spanRow[c]!.rowSpan + cells.push(rc) + widths.push(w) + pads.push(col?.cellPadding ?? DEFAULT_CELL_PADDING) + c += span + } + return { cells, geo: { widths, pads } } +} + +/* -------------------------------- 行高测量 ------------------------------- */ + +function measureRowHeight( + { cells, geo }: BuiltRow, + control: TableControl, + measurer: TextMeasurer, +): number { + const mode = control.options?.rowHeightMode ?? 'auto' + if (mode === 'fixed') { + return Math.max(MIN_ROW_HEIGHT, control.options?.rowHeight ?? 8) + } + + let maxH = 0 + for (let i = 0; i < cells.length; i++) { + const cell = cells[i]! + const avail = Math.max(1, (geo.widths[i] ?? 0) - (geo.pads[i] ?? DEFAULT_CELL_PADDING) * 2) + const { heightMm } = measurer.measure(cell.text ?? '', { + fontSize: cell.fontSize ?? TABLE_FONT_SIZE, + fontWeight: cell.bold ? 'bold' : 'normal', + widthMm: avail, + }) + if (heightMm > maxH) maxH = heightMm + } + return Math.max(MIN_ROW_HEIGHT, maxH + CELL_PADDING_Y * 2) +} + +/* -------------------------------- 建模 -------------------------------- */ + +export interface BuildTableOptions { + control: TableControl + ctx: EvalContext + measurer: TextMeasurer + /** + * 表格宽度(mm)。协议 page.unit 可能是 in/pt,而测量器只认 mm, + * 因此调用方必须把换算后的值传进来;缺省时按 control.width 已是 mm 处理。 + */ + widthMm?: number + /** 表格高度(mm),仅布局网格算行数时用到 */ + heightMm?: number +} + +export function buildTableModel({ + control, + ctx, + measurer, + widthMm, + heightMm, +}: BuildTableOptions): TableModel { + const warnings: RenderWarning[] = [] + const columns = control.columns ?? [] + const tableWidth = widthMm ?? control.width + const tableHeight = heightMm ?? control.height + const columnWidths = normalizeColumnWidths(columns, tableWidth) + const errors: string[] = [] + + // 设计期网格 = 内容与样式的唯一真理源(无 cells 的老模板由列配置瞬时推导) + const grid = buildDesignGrid(control) + const colCount = grid.colCount + // 整张网格的合并布局(colSpan + rowSpan 一起算,设计 overlay 与运行期引擎共用同一份) + const spanLayout = computeSpanLayout(grid.cells, grid.rowCount, colCount) + + const buildRowFrom = ( + rowCells: TableCell[], + spanRow: CellSpan[], + make: (cell: TableCell, col: TableColumn | undefined, colIndex: number) => RenderCell, + ): BuiltRow => layoutDesignRow(rowCells, spanRow, columns, columnWidths, colCount, make) + + /* ── 表头行(支持多行表头) ── */ + const headerRows: RenderRow[] = [] + for (let r = 0; r < grid.headerRows; r++) { + const built = buildRowFrom(grid.cells[r] ?? [], spanLayout[r]!, (cell, col) => + applyCellStyle( + { + text: staticCellText(cell, col, col?.title ?? '', ctx, errors), + align: col?.headerAlign ?? col?.align ?? 'center', + background: col?.headerBackgroundColor, + bold: true, + }, + control, + col, + cell, + 'header', + ), + ) + headerRows.push({ + kind: 'header', + height: measureRowHeight(built, control, measurer) * HEADER_ROW_FACTOR, + cells: built.cells, + }) + } + + /** 静态行(布局网格正文 / 数据表静态尾行):字面量 + 全局插值 */ + const buildStaticRow = (rowCells: TableCell[], height: number | undefined, spanRow: CellSpan[]): RenderRow => { + const built = buildRowFrom(rowCells, spanRow, (cell, col) => + applyCellStyle( + { text: staticCellText(cell, col, '', ctx, errors), align: col?.align ?? 'left' }, + control, + col, + cell, + 'static', + ), + ) + return { + kind: 'static', + height: height ?? measureRowHeight(built, control, measurer), + cells: built.cells, + } + } + + const flushErrors = (): void => { + for (const message of new Set(errors)) { + warnings.push({ code: 'EXPRESSION_ERROR', message, controlId: control.id }) + } + } + + /* ── 布局网格:无数据源,行高与设计画布逐行对齐(§5.4) ── */ + const isLayoutGrid = !isDataTable(control) + if (isLayoutGrid) { + // 与设计期共用 designRowHeights:画布上是几毫米,打印就是几毫米 + const heights = designRowHeights(control, tableHeight) + headerRows.forEach((row, i) => { + row.height = heights[i] ?? row.height + }) + const rows: RenderRow[] = [] + for (let r = grid.headerRows; r < grid.rowCount; r++) { + rows.push(buildStaticRow(grid.cells[r] ?? [], heights[r], spanLayout[r]!)) + } + flushErrors() + return { control, columns, columnWidths, headerRows, rows, footerRows: [], dataRows: [], warnings, isLayoutGrid: true } + } + + /* ── 数据行 ── */ + let dataRows = resolveRows(control, ctx, warnings) + + if (control.options?.skipEmptyRows) { + const fields = columns.map((c) => c.field).filter((f): f is string => Boolean(f)) + dataRows = dataRows.filter((r) => !isEmptyRow(r, fields)) + } + + const plans = planRows({ control, rows: dataRows, ctx }) + + /** 与列 1:1 对齐的行(组头 / 小计 / 合计)的测量几何 */ + const columnGeo: RowGeometry = { + widths: columnWidths, + pads: columns.map((c) => c.cellPadding ?? DEFAULT_CELL_PADDING), + } + + // 数据样例行(模板):决定每个数据行的取值方式与单元格样式 + const template = grid.cells[grid.headerRows] ?? [] + // 数据行由运行期逐条生成,跨行会跨越不同记录,语义不成立 → 强制模板行 rowSpan=1(colSpan 保留) + const templateSpan: CellSpan[] = spanLayout[grid.headerRows]!.map((s) => ({ ...s, rowSpan: 1 })) + + const rows: RenderRow[] = plans.map((plan) => buildRow(plan)) + + function buildRow(plan: RowPlan): RenderRow { + if (plan.kind === 'group') { + const text = plan.label ?? '' + const cells: RenderCell[] = [ + { text, align: 'left', colSpan: colCount, bold: true, background: '#F5F7FA' }, + ] + return { + kind: 'group', + height: measureRowHeight( + { cells, geo: { widths: [tableWidth], pads: [columnGeo.pads[0] ?? DEFAULT_CELL_PADDING] } }, + control, + measurer, + ), + cells, + } + } + + if (plan.kind === 'subtotal' || plan.kind === 'summary') { + const style = + plan.kind === 'subtotal' ? control.options?.summaryRow?.subtotalStyle : undefined + const cells = buildAggregateCells(plan.label ?? '小计', plan.aggregates ?? {}, style) + return { + kind: plan.kind, + height: measureRowHeight({ cells, geo: columnGeo }, control, measurer), + cells, + } + } + + // 数据行:内容与样式都由"数据样例行"模板驱动(含 colSpan 合并;rowSpan 强制为 1) + const rowCtx: EvalContext = { ...ctx, row: plan.row, rowIndex: plan.dataIndex ?? 0 } + const built = buildRowFrom(template, templateSpan, (cell, col) => + applyCellStyle( + { + text: dataCellText(cell, col, rowCtx, errors), + align: col?.align ?? 'left', + background: col?.cellBackgroundColor, + }, + control, + col, + cell, + 'data', + ), + ) + return { + kind: 'data', + dataIndex: plan.dataIndex, + height: measureRowHeight(built, control, measurer), + cells: built.cells, + } + } + + /** + * 合计行按列填值:第一列放标签,配置了聚合的列放数值,其余留空。 + * 若第一列本身就是聚合列,则标签退到"无处可放",此时把标签并入该列前缀。 + * @param style 可选样式覆盖(如分组小计行自定义样式),只覆盖显式设置的项。 + */ + function buildAggregateCells( + label: string, + agg: Record, + style?: TableCellStyle, + ): RenderCell[] { + const applyStyle = (cell: RenderCell): RenderCell => { + if (!style) return cell + const out: RenderCell = { ...cell } + if (style.bold !== undefined) out.bold = style.bold + if (style.italic !== undefined) out.italic = style.italic + if (style.underline !== undefined) out.underline = style.underline + if (style.fontSize !== undefined) out.fontSize = style.fontSize + if (style.fontFamily) out.fontFamily = style.fontFamily + if (style.color) out.color = style.color + if (style.backgroundColor) out.background = style.backgroundColor + if (style.align) out.align = style.align + if (style.valign) out.valign = style.valign + return out + } + + const cells: RenderCell[] = columns.map((c, i) => { + const field = c.field + const hit = + field !== undefined && (field in agg ? field : stripPrefix(field) in agg ? stripPrefix(field) : null) + if (hit) { + return applyStyle({ + text: formatAggregate(agg[hit] ?? 0), + align: c.align ?? 'right', + bold: true, + }) + } + return applyStyle({ + text: i === 0 ? label : '', + align: i === 0 ? 'left' : (c.align ?? 'left'), + bold: true, + }) + }) + if (cells.length === 0) { + cells.push(applyStyle({ text: label, align: 'left', bold: true, colSpan: 1 })) + } + return cells + } + + function stripPrefix(field: string): string { + return field.includes('[].') ? field.slice(field.indexOf('[].') + 3) : field + } + + /* ── 表尾块:聚合尾行(本页合计 / 总计 / 大写金额)+ 设计期静态尾行 ── */ + + /** + * 构建一行表尾(语义尾行)。若该静态行含聚合 token,则按列字段类型判断是否需要计算: + * - 数值列 → 标记 isAgg + aggField,渲染期由切片器按本页/总计重算; + * - 非数值列(字符串等)→ 不计算,渲染为空占位。 + * 首列的行名标签("本页合计"/"总计"/"大写金额")按普通静态文本处理。 + */ + function buildFooterRow(rowCells: TableCell[], spanRow: CellSpan[]): RenderRow { + const tokens = rowCells.map((c) => parseAggToken(c.text)) + const hasToken = tokens.some((t) => t !== null) + let fk: FooterKind = 'static' + if (hasToken) { + if (tokens.some((t) => t === 'pageCap' || t === 'totalCap')) fk = 'capital' + else if (tokens.some((t) => t === 'totalSum' || t === 'totalAvg' || t === 'totalCount')) + fk = 'grandTotal' + else fk = 'pageSubtotal' + } + const built = buildRowFrom(rowCells, spanRow, (cell, col) => { + const tk = parseAggToken(cell.text) + if (tk) { + const field = stripItems(col?.field) + const numeric = columnAggregatable(col, dataRows[0]) + if (numeric && field) { + return applyCellStyle( + { + text: '', + align: col?.align ?? 'right', + bold: true, + isAgg: true, + tokenKind: tk, + aggField: field, + }, + control, + col, + cell, + 'static', + ) + } + // 非数值列:聚合 token 不参与计算,留空 + return applyCellStyle( + { text: '', align: col?.align ?? 'left', bold: true }, + control, + col, + cell, + 'static', + ) + } + return applyCellStyle( + { text: staticCellText(cell, col, '', ctx, errors), align: col?.align ?? 'left' }, + control, + col, + cell, + 'static', + ) + }) + return { + kind: fk === 'static' ? 'static' : fk === 'pageSubtotal' ? 'subtotal' : 'summary', + footerKind: fk, + height: measureRowHeight(built, control, measurer), + cells: built.cells, + } + } + + const footerRows: RenderRow[] = [] + const summaryPlan = buildSummaryPlan(dataRows, control) + if (summaryPlan) { + const cells = buildAggregateCells(summaryLabel(control), summaryPlan.aggregates ?? {}) + footerRows.push({ + kind: 'summary', + footerKind: 'static', + height: measureRowHeight({ cells, geo: columnGeo }, control, measurer), + cells, + }) + } + const sample = dataRows[0] + for (let r = grid.headerRows + 1; r < grid.rowCount; r++) { + footerRows.push(buildFooterRow(grid.cells[r] ?? [], spanLayout[r]!)) + } + + flushErrors() + + return { control, columns, columnWidths, headerRows, rows, footerRows, dataRows, warnings, isLayoutGrid: false } +} + +/* -------------------------------- 切片 -------------------------------- */ + +export interface SliceRequest { + /** 本页分配给表格的可用高度(mm) */ + avail: number + /** 从 model.rows 的第几行开始 */ + start: number +} + +export interface SliceResult { + headerRows: RenderRow[] + rows: RenderRow[] + footerRows: RenderRow[] + /** 下一片起始下标 */ + nextStart: number + /** 本片实际占高(mm) */ + height: number + isLast: boolean + warnings: RenderWarning[] +} + +/** 表格在一页上至少要占的高度(表头 + 1 行 + 表尾),用于 keepTogether 判定 */ +export function minStartHeight(model: TableModel): number { + const h = sumRowHeights(model.headerRows) + const firstRow = model.rows[0]?.height ?? MIN_ROW_HEIGHT + const footer = sumRowHeights(model.footerRows) + return h + firstRow + footer +} + +/** + * 从 start 位置切出一页能放下的内容。 + * + * 预算顺序:可用高 −(表头)−(表尾)= 数据行预算。 + * 表尾是否占预算取决于 repeatFooter: + * - repeatFooter !== false → 每页都有表尾,恒占预算 + * - repeatFooter === false → 只有末片有表尾,先按"无表尾"试算, + * 若恰好切到最后一行,再回头检查表尾能否塞下,塞不下就吐出若干行留到下一页 + */ +export function sliceTable(model: TableModel, req: SliceRequest): SliceResult { + const warnings: RenderWarning[] = [] + const opts = model.control.options ?? {} + const isFirst = req.start === 0 + + const repeatHeader = opts.repeatHeader ?? true + const repeatFooter = opts.repeatFooter ?? true + + const headerRows = isFirst || repeatHeader ? model.headerRows : [] + const headerH = sumRowHeights(headerRows) + // 尾行按"是否每页都出现"拆分预算,避免非末页为总计/大写行预留留白: + // - 每页固定:本页合计 + 静态尾行(repeatFooter 时) + // - 仅末页:总计 + 大写金额 + 静态尾行(repeatFooter 关时) + const pageFooterH = model.footerRows + .filter((f) => f.footerKind === 'pageSubtotal' || (f.footerKind === 'static' && repeatFooter)) + .reduce((s, r) => s + r.height, 0) + const lastFooterH = model.footerRows + .filter( + (f) => + f.footerKind === 'grandTotal' || + f.footerKind === 'capital' || + (f.footerKind === 'static' && !repeatFooter), + ) + .reduce((s, r) => s + r.height, 0) + + // 固定每页行数(pageRows 为数字时强制分页) + const forcedRows = + typeof opts.pageRows === 'number' && opts.pageRows > 0 ? Math.floor(opts.pageRows) : null + + let budget = req.avail - headerH - pageFooterH + + // 把尾行模板按当前切片重算(本页合计用本页数据行;总计/大写用全表数据行,仅末页) + function resolveTailRow(fr: RenderRow, rows: Array>): RenderRow { + const cells: RenderCell[] = fr.cells.map((cell) => { + if (!cell.isAgg || !cell.aggField || !cell.tokenKind) return cell + const text = + cell.tokenKind === 'totalCap' || cell.tokenKind === 'pageCap' + ? toChineseCapitalRMB(aggValueForRows('totalSum', rows, cell.aggField)) + : formatAggNumber(aggValueForRows(cell.tokenKind, rows, cell.aggField)) + return { ...cell, text } + }) + return { ...fr, cells } + } + + function resolveFooterRows( + pickedDataRows: Array>, + last: boolean, + ): RenderRow[] { + const out: RenderRow[] = [] + for (const fr of model.footerRows) { + const fk = fr.footerKind + if (fk === 'pageSubtotal') { + out.push(resolveTailRow(fr, pickedDataRows)) + } else if (fk === 'grandTotal' || fk === 'capital') { + if (last) out.push(resolveTailRow(fr, model.dataRows)) + } else if (repeatFooter || last) { + out.push(fr) + } + } + return out + } + + const picked: RenderRow[] = [] + let used = 0 + let i = req.start + + while (i < model.rows.length) { + const row = model.rows[i]! + if (forcedRows !== null && picked.length >= forcedRows) break + + if (used + row.height > budget) { + // 一行都放不下:强制放一行,避免无限循环(并告警) + if (picked.length === 0) { + if (row.height > req.avail) { + warnings.push({ + code: 'ROW_TOO_TALL', + message: `第 ${(row.dataIndex ?? i) + 1} 行高度 ${row.height.toFixed(1)}mm 超过整页可用高度,已强制放置`, + controlId: model.control.id, + }) + } + picked.push(row) + used += row.height + i++ + } + break + } + + picked.push(row) + used += row.height + i++ + } + + let isLast = i >= model.rows.length + let footerRows: RenderRow[] = [] + + // 仅当本片确有数据行时才挂表尾;否则空尾片(上一页已放完全部行)会渲染出多余的合计 + if (model.footerRows.length > 0 && picked.length > 0) { + // 本片涉及的数据行(映射回原始数据,供本页合计重算) + const pickedDataRows = picked + .filter((r) => r.kind === 'data' && r.dataIndex !== undefined) + .map((r) => model.dataRows[r.dataIndex!]!) + .filter(Boolean) + + if (isLast) { + // 末页需额外容纳总计 / 大写金额行:放不下则把数据行让给下一页 + while (picked.length > 0 && headerH + used + pageFooterH + lastFooterH > req.avail) { + const removed = picked.pop()! + used -= removed.height + i-- + } + if (headerH + used + pageFooterH + lastFooterH <= req.avail || picked.length === 0) { + footerRows = resolveFooterRows(pickedDataRows, true) + } else { + isLast = false + footerRows = resolveFooterRows(pickedDataRows, false) + } + } else { + footerRows = resolveFooterRows(pickedDataRows, false) + } + } + + isLast = isLast && i >= model.rows.length + + return { + headerRows, + rows: picked, + footerRows, + nextStart: i, + height: headerH + used + sumRowHeights(footerRows), + isLast, + warnings, + } +} + +/** 表格全部内容一次性排开的总高度(用于判断是否需要分页) */ +export function totalTableHeight(model: TableModel): number { + return sumRowHeights(model.headerRows) + sumRowHeights(model.rows) + sumRowHeights(model.footerRows) +} + +export type { HAlign } diff --git a/openprint/src/core/layout-engine/types.ts b/openprint/src/core/layout-engine/types.ts new file mode 100644 index 0000000..c8f1b13 --- /dev/null +++ b/openprint/src/core/layout-engine/types.ts @@ -0,0 +1,192 @@ +/** + * Layout Engine 内部页面模型 —— 《OpenPrint-设计方案.md》§7.3 的类型化扩展 + * + * 设计要点: + * 1. 引擎内部一律用 **mm** 参与排版(协议层 in/pt 在入口换算,见 core/units.ts)。 + * 2. 输出是「已定位、已绑定、已测量」的纯数据,不含任何 DOM / Fabric 依赖, + * 因此 renderer-html 可被替换为 renderer-svg / renderer-pdf 而无需改引擎。 + * 3. 坐标系:PlacedNode.left/top 相对**所属 section 左上角**(与协议一致)。 + */ +import type { AnyControl, HAlign, TableColumn, TableControl } from '@/types/control' +import type { PageSetup } from '@/types/template' + +/* -------------------------------- 几何 -------------------------------- */ + +/** 矩形盒(mm) */ +export interface Box { + left: number + top: number + width: number + height: number +} + +/* ------------------------------ 渲染就绪内容 ------------------------------ */ + +/** + * 控件绑定数据后的「渲染就绪内容」。 + * 到这一步所有异步资源(二维码/条码 SVG)已解析完成,渲染器只做字符串拼装。 + */ +export type ResolvedContent = + | { kind: 'text'; text: string } + /** 富文本:已经过 DOMPurify 消毒的 HTML */ + | { kind: 'html'; html: string } + | { kind: 'image'; src: string } + /** 条码 / 二维码矢量输出(打印必须矢量,位图会糊) */ + | { kind: 'svg'; svg: string } + /** 纯形状(矩形 / 线条),样式由 control 决定 */ + | { kind: 'shape' } + /** 解析失败的占位(渲染为虚线框 + 提示文字,不静默吞掉) */ + | { kind: 'placeholder'; label: string } + +/* ------------------------------- 已定位节点 ------------------------------- */ + +export interface PlacedControl extends Box { + kind: 'control' + id: string + angle?: number + content: ResolvedContent + /** 原始控件,渲染器据此取样式(fill/stroke/fontSize…) */ + control: AnyControl +} + +/** + * 表格行类型:决定样式(加粗/底色)与分页时能否被切开。 + * `static` = 设计期填写的静态行(布局网格正文行、数据表静态尾行如备注/签字栏)。 + */ +export type RenderRowKind = 'header' | 'data' | 'group' | 'subtotal' | 'summary' | 'static' + +export interface RenderCell { + text: string + align: HAlign + /** 跨列(组头行 / 合计行用) */ + colSpan?: number + /** 跨行(表头 / 静态 / 布局网格单元格合并用) */ + rowSpan?: number + background?: string + bold?: boolean + /** 字号(pt) */ + fontSize?: number + fontFamily?: string + italic?: boolean + underline?: boolean + color?: string + valign?: 'top' | 'middle' | 'bottom' + /** 单元格斜线(课表角标等):none / down(左上→右下) / up(左下→右上) */ + diagonal?: 'none' | 'down' | 'up' + /** 尾行聚合:该单元格是否为聚合 token({{#totalSum}} 等) */ + isAgg?: boolean + /** 聚合 token 类型(isAgg 为真时有效) */ + tokenKind?: import('./aggregate').AggKind + /** 聚合对应的数据字段键(已去 items[]. 前缀);isAgg 为真时有效 */ + aggField?: string +} + +export interface RenderRow { + kind: RenderRowKind + /** 行高(mm),已由 TableEngine 测量 */ + height: number + cells: RenderCell[] + /** 数据行在原数组中的下标(供 striped 斑马纹判定) */ + dataIndex?: number + /** 表尾块内语义:本页合计 / 总计 / 大写金额 / 静态尾行(仅 footerRows 有意义) */ + footerKind?: 'pageSubtotal' | 'grandTotal' | 'capital' | 'static' +} + +/** 表格在某一页上的切片 */ +export interface PlacedTable extends Box { + kind: 'table' + id: string + control: TableControl + columns: TableColumn[] + /** 归一化后的各列实际宽度(mm,合计 === 表格宽度),渲染器直接用,不再二次推算 */ + columnWidths: number[] + /** 本页表头(可多行;repeatHeader=false 时仅首片有值) */ + headerRows: RenderRow[] + /** 本页数据行切片 */ + rows: RenderRow[] + /** 本页表尾块:合计行 + 静态尾行(repeatFooter=false 时仅末片有值) */ + footerRows: RenderRow[] + /** 是否为该表格的最后一片(渲染器可据此决定收口边框) */ + isLastSlice: boolean +} + +export type PlacedNode = PlacedControl | PlacedTable + +/* --------------------------------- 页 --------------------------------- */ + +export interface LayoutPage { + /** 0-based 索引 */ + index: number + /** 1-based 页码(页码变量 {{page}} 的值) */ + pageNo: number + header: PlacedControl[] + body: PlacedNode[] + footer: PlacedControl[] +} + +/* -------------------------------- 页面度量 ------------------------------- */ + +/** + * 一页的实际可用区域(全部 mm,width/height 为物理尺寸,横向时宽>高)。 + * 这是分页算法的唯一尺寸真理源。 + */ +export interface PageMetrics { + /** 纸张实际宽高(物理尺寸,不随 orientation 再交换) */ + pageWidth: number + pageHeight: number + margin: { top: number; right: number; bottom: number; left: number } + /** 页边距内的内容区宽度 */ + contentWidth: number + /** 页边距内的内容区高度(含页眉页脚) */ + contentHeight: number + headerHeight: number + footerHeight: number + /** 正文可用高度 = contentHeight - headerHeight - footerHeight */ + bodyHeight: number +} + +/* -------------------------------- 警告 -------------------------------- */ + +export type WarningCode = + | 'BINDING_MISSING' // 绑定路径在数据中不存在 + | 'EXPRESSION_ERROR' // 表达式求值失败 + | 'DATASOURCE_NOT_ARRAY' // 表格 dataSource 不是数组 + | 'DATASOURCE_EMPTY' // 表格数据为空 + | 'CONTENT_OVERFLOW' // 内容超出可用高度被截断 + | 'IMAGE_UNRESOLVED' // 图片无法解析 + | 'BARCODE_FAILED' // 条码/二维码生成失败 + | 'CHART_FAILED' // 图表 SVG 生成失败 + | 'PAGE_LIMIT_REACHED' // 触发最大页数保护 + | 'ROW_TOO_TALL' // 单行高于整页可用高度 + +export interface RenderWarning { + code: WarningCode + message: string + controlId?: string +} + +/* ------------------------------- 引擎产物 ------------------------------- */ + +export interface LayoutResult { + pages: LayoutPage[] + metrics: PageMetrics + page: PageSetup + warnings: RenderWarning[] +} + +/** 求值上下文:数据根 + 行上下文 + 页码变量 */ +export interface EvalContext { + /** 业务数据根对象 */ + data: Record + /** 当前行(表格单元格求值时注入) */ + row?: Record + /** 当前行下标(0-based) */ + rowIndex?: number + /** 当前页码(1-based) */ + page?: number + /** 总页数 */ + pages?: number +} + +/** 分页保护上限:防止死循环 / 恶意数据把浏览器打爆 */ +export const MAX_PAGES = 500 diff --git a/openprint/src/core/print-client/client.ts b/openprint/src/core/print-client/client.ts new file mode 100644 index 0000000..4091f3c --- /dev/null +++ b/openprint/src/core/print-client/client.ts @@ -0,0 +1,210 @@ +/** + * 本地打印机客户端 HTTP 客户端 —— 纯 TypeScript,零框架依赖 + * + * 所有方法都显式接收 baseUrl(由 `@/config/printer` 解析), + * 便于 headless / 单测直接注入,不隐式读全局配置。 + */ +import { + PrintClientError, + type PrinterHealth, + type PrinterInfo, + type PrinterListResponse, + type PrintJobRequest, + type PrintJobResponse, +} from './types' + +/** 出厂默认地址(用户未配置时使用) */ +export const DEFAULT_PRINTER_BASE_URL = 'http://127.0.0.1:18888' + +/** 探测类请求默认超时(ms)—— 本机服务,短超时快速失败 */ +export const PROBE_TIMEOUT_MS = 3000 + +/** 提交打印任务默认超时(ms)—— 大文档 base64 传输留足时间 */ +export const SUBMIT_TIMEOUT_MS = 30000 + +/** 去掉尾部斜杠,拼接路径 */ +function joinUrl(base: string, path: string): string { + return `${base.replace(/\/+$/, '')}${path}` +} + +/** + * 带超时 + 错误分类的 fetch。 + * 用手工 AbortController 而非 AbortSignal.timeout,才能区分「超时」与「连不上」。 + */ +async function request( + url: string, + init: RequestInit, + timeoutMs: number, +): Promise { + const ctrl = new AbortController() + let timedOut = false + const timer = setTimeout(() => { + timedOut = true + ctrl.abort() + }, timeoutMs) + + let res: Response + try { + res = await fetch(url, { ...init, signal: ctrl.signal }) + } catch { + if (timedOut) { + throw new PrintClientError('timeout', `请求超时(${timeoutMs}ms):${url}`) + } + throw new PrintClientError('network', `无法连接打印客户端:${url}`) + } finally { + clearTimeout(timer) + } + + if (!res.ok) { + throw new PrintClientError('http', `HTTP ${res.status} ${res.statusText}`, res.status) + } + + let json: unknown + try { + json = await res.json() + } catch { + throw new PrintClientError('parse', '响应不是合法 JSON') + } + if (json === null || typeof json !== 'object') { + throw new PrintClientError('parse', '响应结构异常') + } + return json as T +} + +/* ------------------------------ GET /health ------------------------------ */ + +/** + * 健康检查。成功返回服务版本、打印机数量、运行时长。 + * @throws PrintClientError 连接失败 / 超时 / ok:false + */ +export async function checkHealth( + baseUrl: string = DEFAULT_PRINTER_BASE_URL, + timeoutMs: number = PROBE_TIMEOUT_MS, +): Promise { + const data = await request>( + joinUrl(baseUrl, '/health'), + { method: 'GET', headers: { Accept: 'application/json' } }, + timeoutMs, + ) + if (data.ok !== true) { + throw new PrintClientError('service', '打印客户端报告服务异常(ok=false)') + } + return { + app: data.app ?? 'Unknown', + ok: true, + printers: typeof data.printers === 'number' ? data.printers : 0, + time: data.time ?? '', + uptimeSec: typeof data.uptimeSec === 'number' ? data.uptimeSec : 0, + version: data.version ?? '', + } +} + +/* ------------------------------ GET /printers ------------------------------ */ + +/** 单台打印机字段归一化(服务端字段缺失时给安全默认,避免 UI 崩) */ +function normalizePrinter(raw: Partial, index: number): PrinterInfo { + return { + driver: raw.driver ?? '', + isDefault: raw.isDefault === true, + isOnline: raw.isOnline !== false, + kind: raw.kind === 'virtual' || raw.kind === 'ticket' ? raw.kind : 'common', + maxDpi: typeof raw.maxDpi === 'number' ? raw.maxDpi : 0, + name: raw.name ?? `未命名打印机 ${index + 1}`, + status: raw.status === 'error' ? 'error' : 'idle', + supportsColor: raw.supportsColor === true, + supportsDuplex: raw.supportsDuplex === true, + trays: Array.isArray(raw.trays) ? raw.trays.filter((t): t is string => typeof t === 'string') : [], + } +} + +/** + * 获取打印机列表。 + * 注意:**以 `printers` 数组长度为准**,服务端的 `count` 字段仅供参考(实测可能不一致)。 + */ +export async function listPrinters( + baseUrl: string = DEFAULT_PRINTER_BASE_URL, + timeoutMs: number = PROBE_TIMEOUT_MS, +): Promise { + const data = await request>( + joinUrl(baseUrl, '/printers'), + { method: 'GET', headers: { Accept: 'application/json' } }, + timeoutMs, + ) + if (data.ok === false) { + throw new PrintClientError('service', '打印客户端未能枚举打印机') + } + if (!Array.isArray(data.printers)) { + throw new PrintClientError('parse', '响应缺少 printers 数组') + } + return data.printers.map((p, i) => normalizePrinter(p ?? {}, i)) +} + +/* ------------------------------ 任务号生成 ------------------------------ */ + +/** + * 生成打印任务号(Web 端生成,随请求下发给本地客户端)。 + * 格式:`MMDD`(今日月日,如 0813)+ 6 位随机数字,共 10 位。 + * 例:`0813` + `482913` → `0813482913`。 + * @param now 可选,默认当前时间(便于单测注入) + */ +export function generateJobId(now: Date = new Date()): string { + const mm = String(now.getMonth() + 1).padStart(2, '0') + const dd = String(now.getDate()).padStart(2, '0') + const rand = String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0') + return `${mm}${dd}${rand}` +} + +/* ------------------------------ POST /print ------------------------------ */ + +/** + * 提交打印任务。 + * 载荷约定(见《打印机数据交互文档》): + * - **统一** `format:'pdf'`,`encoding:'base64'`,content 为 PDF base64(无 data: 前缀)。 + * PDF 内部位图底图默认 PNG(无损·最高清),文字/线条边缘真正锐利,打印推荐。 + * 本地客户端用 `QPrinter` 打印位图 PDF。 + * - `'svg'` 已废弃(Qt QSvgRenderer 不支持 foreignObject/HTML/CSS),类型保留仅作向后兼容。 + * - `job.jobId` 由 Web 端生成(generateJobId);服务端若回传自己的 jobId 则以服务端为准。 + */ +export async function submitPrintJob( + job: PrintJobRequest, + baseUrl: string = DEFAULT_PRINTER_BASE_URL, + timeoutMs: number = SUBMIT_TIMEOUT_MS, +): Promise { + const data = await request>( + joinUrl(baseUrl, '/print'), + { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(job), + }, + timeoutMs, + ) + if (data.ok !== true) { + throw new PrintClientError('service', data.message || '打印客户端拒绝了本次任务') + } + return { ok: true, jobId: data.jobId ?? job.jobId, message: data.message } +} + +/* ------------------------------ 工具 ------------------------------ */ + +/** + * Blob → base64(不含 `data:*;base64,` 前缀)。 + * 用于把 PDF 二进制转成可 JSON 传输的字符串。 + */ +export function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onerror = () => reject(new Error('读取导出文件失败')) + reader.onload = () => { + const result = String(reader.result ?? '') + const comma = result.indexOf(',') + resolve(comma >= 0 ? result.slice(comma + 1) : result) + } + reader.readAsDataURL(blob) + }) +} + +/** Blob → utf8 文本(用于 SVG 载荷) */ +export function blobToText(blob: Blob): Promise { + return blob.text() +} diff --git a/openprint/src/core/print-client/index.ts b/openprint/src/core/print-client/index.ts new file mode 100644 index 0000000..6badd01 --- /dev/null +++ b/openprint/src/core/print-client/index.ts @@ -0,0 +1,42 @@ +/** + * 本地打印机客户端服务层入口 + * + * 用法: + * ```ts + * import { checkHealth, listPrinters, submitPrintJob } from '@/core/print-client' + * import { resolvePrinterBaseUrl } from '@/config/printer' + * + * const base = resolvePrinterBaseUrl() + * const health = await checkHealth(base) + * const printers = await listPrinters(base) + * ``` + */ +export { + DEFAULT_PRINTER_BASE_URL, + PROBE_TIMEOUT_MS, + SUBMIT_TIMEOUT_MS, + checkHealth, + listPrinters, + submitPrintJob, + generateJobId, + blobToBase64, + blobToText, +} from './client' + +export { PrintClientError, describePrintError } from './types' + +export { buildPrintPayload, formatPayloadSize } from './payload' +export type { PrintPayload, BuildPrintPayloadOptions } from './payload' + +export type { + PrinterHealth, + PrinterInfo, + PrinterKind, + PrinterState, + PrinterListResponse, + PrintPayloadFormat, + PrintPayloadEncoding, + PrintJobRequest, + PrintJobResponse, + PrintClientErrorCode, +} from './types' diff --git a/openprint/src/core/print-client/payload.ts b/openprint/src/core/print-client/payload.ts new file mode 100644 index 0000000..2591d9e --- /dev/null +++ b/openprint/src/core/print-client/payload.ts @@ -0,0 +1,79 @@ +/** + * 打印载荷构建 —— 模板 + 数据 → 推送给本地打印客户端的文档内容 + * + * 推送格式规则(2026-08-13 最终定案): + * - **统一推 PDF(base64)**,不论单页或多页。 + * + * 演进历程: + * - 早期单页走 SVG 原文 → Qt `QSvgRenderer` 不解析 foreignObject/HTML/CSS,打不出内容(已废弃)。 + * - 中间试过单页走 JPG → 但 JPG 是单张位图、没有页面尺寸/DPI 元数据,不如 PDF 适合打印; + * 且用户希望打印格式统一便于客户端处理。最终回到统一 PDF。 + * + * 清晰度优先:PDF 内部位图底图默认 **PNG(无损·最高清)**,文字/线条边缘真正锐利, + * 打印推荐。体积较大(纯文字页 ~10+ MB);体积敏感场景可传 `imageType: 'jpeg'`。 + */ +import type { RenderRequest } from '@/core/sdk' +import { renderDocument } from '@/core/sdk' +import { documentToPdf } from '@/core/export-engine' +import { blobToBase64 } from './client' + +export interface PrintPayload { + /** 载荷格式:恒为 pdf */ + format: 'pdf' + /** 载荷编码:恒为 base64 */ + encoding: 'base64' + /** PDF 的 base64 字符串(不含 data: 前缀) */ + content: string + /** 总页数 */ + pages: number + /** 载荷字符长度(UI 展示体积用) */ + bytes: number +} + +export interface BuildPrintPayloadOptions { + /** 位图倍率,默认 3(288dpi·最高清);要更锐可传 4 */ + scale?: number + /** + * 多页 PDF 的位图底图压缩格式(默认 `'png'`,无损最清晰): + * - `'png'`:无损,文字/线条边缘锐利,打印推荐; + * - `'jpeg'`:体积小,仅体积敏感场景。 + */ + imageType?: 'jpeg' | 'png' +} + +/** + * 构建推送载荷(统一 PDF-base64)。 + * + * 不论单页或多页,都先 `renderDocument()` → `documentToPdf()`(浏览器内用 Chromium + * 栅格化 foreignObject 内容成位图底图,默认 PNG 无损最高清)→ `blobToBase64()`。 + * 本地客户端拿到的就是可直接 `QPrinter` 打印的 PDF 字节流。 + */ +export async function buildPrintPayload( + request: RenderRequest, + options: BuildPrintPayloadOptions = {}, +): Promise { + const result = await renderDocument(request) + const pages = result.pages.length + const deco = request.output?.pageDecoration + + const blob = await documentToPdf(result, { + scale: options.scale ?? 3, + imageType: options.imageType ?? 'png', + pageDecoration: deco, + }) + const base64 = await blobToBase64(blob) + return { + format: 'pdf', + encoding: 'base64', + content: base64, + pages: Math.max(pages, 1), + bytes: base64.length, + } +} + +/** 人类可读体积(UI 提示用) */ +export function formatPayloadSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / 1024 / 1024).toFixed(2)} MB` +} diff --git a/openprint/src/core/print-client/print-client.spec.ts b/openprint/src/core/print-client/print-client.spec.ts new file mode 100644 index 0000000..ea897ca --- /dev/null +++ b/openprint/src/core/print-client/print-client.spec.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest' +import { generateJobId } from './client' + +describe('generateJobId', () => { + it('格式为 MMDD + 6 位随机,共 10 位数字', () => { + const id = generateJobId(new Date('2026-08-13T10:00:00')) + expect(id).toHaveLength(10) + expect(id).toMatch(/^\d{10}$/) + // 今日 08-13 → 前缀 0813 + expect(id.startsWith('0813')).toBe(true) + }) + + it('随机尾段为 6 位(不足前补 0)', () => { + const id = generateJobId(new Date('2026-01-05T00:00:00')) + expect(id.startsWith('0105')).toBe(true) + expect(id.slice(4)).toMatch(/^\d{6}$/) + }) + + it('多次调用尾段大概率不同', () => { + const seen = new Set() + for (let i = 0; i < 50; i++) seen.add(generateJobId().slice(4)) + // 50 次抽样几乎必然出现碰撞以外的情况;极端碰撞属概率事件,放宽到 >=49 唯一 + expect(seen.size).toBeGreaterThanOrEqual(49) + }) +}) diff --git a/openprint/src/core/print-client/types.ts b/openprint/src/core/print-client/types.ts new file mode 100644 index 0000000..345e655 --- /dev/null +++ b/openprint/src/core/print-client/types.ts @@ -0,0 +1,160 @@ +/** + * 本地打印机客户端服务 —— 协议类型 + * + * 对接本机常驻的打印客户端(默认 http://127.0.0.1:18888),提供: + * GET /health 服务健康与版本 + * GET /printers 打印机列表 + * POST /print 提交打印任务(单页 SVG / 多页 PDF-base64) + * + * 主任铁律:无后端全链路可用 —— 客户端不可达时设计器一切功能照常,只是不能推送打印。 + */ + +/* ------------------------------ /health ------------------------------ */ + +/** GET /health 响应体 */ +export interface PrinterHealth { + /** 客户端应用名,如 "OpenPrint" */ + app: string + /** 服务是否正常 */ + ok: boolean + /** 当前可用打印机数量 */ + printers: number + /** 服务端时间(ISO 字符串) */ + time: string + /** 已运行秒数 */ + uptimeSec: number + /** 客户端版本号 */ + version: string +} + +/* ------------------------------ /printers ------------------------------ */ + +/** 打印机类型:虚拟打印机 / 普通打印机 / 票据打印机 */ +export type PrinterKind = 'virtual' | 'common' | 'ticket' + +/** 打印机运行状态 */ +export type PrinterState = 'idle' | 'error' + +/** 单台打印机描述 */ +export interface PrinterInfo { + /** 驱动名 */ + driver: string + /** 是否系统默认打印机 */ + isDefault: boolean + /** 是否在线 */ + isOnline: boolean + /** 打印机类型 */ + kind: PrinterKind + /** 最大分辨率(DPI) */ + maxDpi: number + /** 打印机名(提交任务时的唯一标识) */ + name: string + /** 运行状态 */ + status: PrinterState + /** 是否支持彩色 */ + supportsColor: boolean + /** 是否支持双面 */ + supportsDuplex: boolean + /** 纸盒列表 */ + trays: string[] +} + +/** GET /printers 响应体 */ +export interface PrinterListResponse { + /** 服务端声明的数量(可能与 printers.length 不一致,以数组为准) */ + count: number + ok: boolean + printers: PrinterInfo[] +} + +/* ------------------------------ /print ------------------------------ */ + +/** + * 推送载荷格式。 + * **2026-08-13 最终定案**:统一推 `'pdf'`(base64)。 + * - 单页/多页都走浏览器栅格化的位图 PDF,本地客户端 `QPrinter` 打印无兼容性问题。 + * - `'svg'` 保留在枚举里仅供向后兼容(已废弃:Qt QSvgRenderer 不解析 foreignObject/HTML)。 + */ +export type PrintPayloadFormat = 'svg' | 'pdf' + +/** 载荷编码:jpg/pdf 用 base64;utf8 仅供向后兼容(对应已废弃的 SVG 原文路径) */ +export type PrintPayloadEncoding = 'utf8' | 'base64' + +/** POST /print 请求体 */ +export interface PrintJobRequest { + /** 任务号:由 Web 端生成(MMDD + 6 位随机,共 10 位);服务端若回传则以其为准,否则沿用此值 */ + jobId?: string + /** 任务名(用于打印队列显示) */ + taskName: string + /** 目标打印机名(取自 /printers 的 name;留空由客户端用系统默认) */ + printer: string + /** 载荷格式 */ + format: PrintPayloadFormat + /** 载荷编码 */ + encoding: PrintPayloadEncoding + /** 文档内容:pdf=PDF base64(不含 data: 前缀);svg=XML 原文(已废弃) */ + content: string + /** 文档总页数 */ + pages: number + /** 打印份数 */ + copies: number + /** 纸张方向 */ + orientation: 'portrait' | 'landscape' + /** 是否双面 */ + duplex: boolean + /** 是否彩色(false = 黑白) */ + color: boolean +} + +/** POST /print 响应体 */ +export interface PrintJobResponse { + ok: boolean + /** 客户端队列任务 ID */ + jobId?: string + /** 服务端消息(失败原因等) */ + message?: string +} + +/* ------------------------------ 错误 ------------------------------ */ + +/** + * 错误分类: + * - `timeout` 请求超时(客户端未响应) + * - `network` 连接失败(服务未启动 / 端口不通 / CORS 拦截) + * - `http` HTTP 非 2xx + * - `parse` 响应不是合法 JSON 或字段缺失 + * - `service` 服务端返回 ok:false + */ +export type PrintClientErrorCode = 'timeout' | 'network' | 'http' | 'parse' | 'service' + +/** 打印客户端统一错误 */ +export class PrintClientError extends Error { + readonly code: PrintClientErrorCode + readonly status?: number + + constructor(code: PrintClientErrorCode, message: string, status?: number) { + super(message) + this.name = 'PrintClientError' + this.code = code + this.status = status + } +} + +/** 把任意异常转成人话(用于 UI 提示) */ +export function describePrintError(err: unknown): string { + if (err instanceof PrintClientError) { + switch (err.code) { + case 'timeout': + return '打印客户端响应超时,请确认服务未卡死' + case 'network': + return '无法连接打印客户端,请确认本机打印服务已启动' + case 'http': + return `打印客户端返回错误(HTTP ${err.status ?? '?'})` + case 'parse': + return '打印客户端返回数据格式异常' + case 'service': + return err.message || '打印客户端拒绝了本次请求' + } + } + return err instanceof Error ? err.message : String(err) +} diff --git a/openprint/src/core/renderer-html/css-generator.spec.ts b/openprint/src/core/renderer-html/css-generator.spec.ts new file mode 100644 index 0000000..4483e0d --- /dev/null +++ b/openprint/src/core/renderer-html/css-generator.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { generateCss, mmv } from '@/core/renderer-html/css-generator' +import { renderDocument } from '@/core/sdk' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => ({ + productCode: `P${i + 1}`, + productName: `商品${i + 1}`, + qty: (i % 5) + 1, + price: 10, + amount: ((i % 5) + 1) * 10, + })) + return { order: { orderNo: 'SO-001' }, items } +} + +describe('generateCss —— 动态样式生成', () => { + const measurer = createCjkMeasurer() + + async function css(opts: Parameters[1]) { + const result = await renderDocument({ template: createDemoTemplate(), data: makeData(10), layout: { measurer } }) + return generateCss(result.metrics, opts) + } + + it('默认输出含 @page 与页面尺寸', async () => { + const out = await css({}) + expect(out).toContain('@page') + expect(out).toContain('mm') + }) + + it('screen 模式注入 --op-scale 缩放变量', async () => { + const out = await css({ screen: true, scale: 2 }) + expect(out).toContain('--op-scale: 2') + }) + + it('screen:false 时 --op-scale 强制为 1(导出/打印不缩放)', async () => { + const out = await css({ screen: false }) + expect(out).toContain('--op-scale: 1') + expect(out).not.toContain('--op-scale: 2') + }) + + it('extraCss 追加到样式表', async () => { + const out = await css({ extraCss: '.x{color:red}' }) + expect(out).toContain('.x{color:red}') + }) + + it('mmv 格式化毫米值', () => { + expect(mmv(10)).toBe('10mm') + expect(mmv(0)).toBe('0mm') + }) +}) diff --git a/openprint/src/core/renderer-html/css-generator.ts b/openprint/src/core/renderer-html/css-generator.ts new file mode 100644 index 0000000..3e336eb --- /dev/null +++ b/openprint/src/core/renderer-html/css-generator.ts @@ -0,0 +1,372 @@ +/** + * CSS 生成器 —— 《OpenPrint-设计方案.md》§5.11(打印样式)/ §7.3(渲染产物) + * + * ## 为什么 CSS 要按模板动态生成 + * + * `@page { size: … }` 的纸张尺寸不能用 CSS 变量(浏览器在解析 `@page` 时不做变量替换), + * 必须把 mm 字面量写死进去。所以每个模板都要生成一份专属样式表。 + * + * ## 与测量器的强耦合约定(改这里必须同步改 measure.ts) + * + * 分页高度是 DomMeasurer 用 `font-size / font-family / line-height / white-space` 量出来的。 + * 渲染时若用另一套字体或行高,实际高度就会和分页假设对不上,表现为**末行被裁**或**页尾空一大块**。 + * 因此表格单元格的字体三件套在这里直接复用 measure.ts / table-engine.ts 的导出常量, + * 而不是重新写一遍字面量。 + */ +import { DEFAULT_FONT_FAMILY, DEFAULT_LINE_HEIGHT } from '@/core/layout-engine/measure' +import { + CELL_PADDING_Y, + DEFAULT_CELL_PADDING, + TABLE_FONT_SIZE, +} from '@/core/layout-engine/table-engine' +import type { PageMetrics } from '@/core/layout-engine/types' +import type { PageDecoration } from '@/types/template' + +/** 设计画布文本控件的默认值(PrintText.styleToFabric),渲染端必须一致才能所见即所得 */ +export const TEXT_DEFAULT_FONT_SIZE = 12 +export const TEXT_DEFAULT_FONT_FAMILY = '"Source Han Sans CN", "PingFang SC", sans-serif' +export const TEXT_DEFAULT_LINE_HEIGHT = 1.16 +export const TEXT_DEFAULT_COLOR = '#000000' + +/** 表格视觉(与设计画布 PrintTable 一致) */ +export const TABLE_BORDER_COLOR = '#333333' +export const TABLE_TEXT_COLOR = '#1f2329' +export const TABLE_HEADER_BG = '#F5F7FA' +/** 通用斑马纹底(原 #FAFAFA 太浅,调深为浅蓝,对比更明显) */ +export const TABLE_STRIPE_BG = '#DCE6F4' +/** 斑马纹·蓝(交替行更深蓝) */ +export const TS_ZEBRA_BLUE_BG = '#C2D4EE' +/** 斑马纹·灰(中性灰交替) */ +export const TS_ZEBRA_GRAY_BG = '#E4E7EB' +/** 隔列着色底 */ +export const TS_BANDED_BG = '#EEF2F8' +/** 绿色表头底 */ +export const TABLE_HEADER_GREEN = '#1F9D57' +/** 单元格斜线颜色(与表格边框同色) */ +export const TABLE_DIAGONAL_COLOR = '#333333' + +/** + * 单元格斜线背景(课表角标等)。 + * 返回可直接拼进 inline style 的「完整声明片段」: + * `background-image:url(...); background-repeat:no-repeat; background-size:100% 100%`。 + * 无斜线返回空串。 + * + * 同时被设计画布(table-design-render)与运行期(html-renderer)复用,保证两端一致。 + * + * 用 SVG data URI 而非 linear-gradient 模拟:SVG 有原生抗锯齿, + * 在非整数像素边界不会产生渐变法常见的锯齿毛刺,清晰度大幅提升。 + * + * ⚠️ 关键:background-image 默认 `background-repeat:repeat`, + * 会把 100×100 的 SVG 平铺满整个单元格 —— 这正是「显示成好几根线」的根因。 + * 必须显式 `no-repeat` + `background-size:100% 100%`,让它只画一条、 + * 且 viewBox 经 preserveAspectRatio=none 精确填满任意宽高比的单元格。 + * + * SVG 内部用双引号;整体经 base64 编码后用单引号包裹 url('...')。 + * base64 不会产生引号,彻底避免与 HTML attribute 的引号冲突。 + */ +export function diagonalBackground( + dir: 'none' | 'down' | 'up' | undefined | null, +): string { + if (!dir || dir === 'none') return '' + const c = TABLE_DIAGONAL_COLOR + // down: 左上角(0,0)→右下角(100,100);up: 左下角(0,100)→右上角(100,0) + const y1 = dir === 'up' ? '100' : '0' + const y2 = dir === 'up' ? '0' : '100' + const svg = `` + const b64 = typeof btoa === 'function' ? btoa(svg) : Buffer.from(svg, 'utf-8').toString('base64') + const url = `url('data:image/svg+xml;base64,${b64}')` + // no-repeat + 100% 100%:只画一条、且精确铺满单元格(见函数注释 ⚠️) + return `background-image:${url};background-repeat:no-repeat;background-size:100% 100%` +} + +export interface CssOptions { + /** + * 屏幕预览模式:给页面加投影、页间距、灰底。 + * 导出 HTML / 无头打印时置 false,避免把预览装饰烤进产物。 + */ + screen?: boolean + /** 预览缩放比(仅 screen 生效,打印时强制 1) */ + scale?: number + /** 追加的用户自定义 CSS */ + extraCss?: string + /** 页面装饰(背景色 + 水印),来自模板 pageSetup */ + pageDecoration?: PageDecoration +} + +/** mm 数值格式化:保留 3 位小数并去掉尾随 0,避免 "12.340000000000001mm" 这种噪声 */ +export function mmv(n: number): string { + const v = Math.round((Number.isFinite(n) ? n : 0) * 1000) / 1000 + return `${v}mm` +} + +/** + * 表格样式块(运行期打印 与 设计期 HTML overlay 共用的唯一真理源)。 + * + * 方案 A 的关键:设计画布上的表格 overlay 与最终打印产物必须由**同一份 CSS** 驱动, + * 否则"所见"与"所印"会随时间漂移。设计期通过 `scope` 前缀把这份规则限制在 overlay 容器内。 + * + * @param scope 作用域选择器前缀(如 `.op-table-overlay `);留空 = 全局(打印产物) + */ +export function tableCss(scope = ''): string { + const s = scope ? `${scope} ` : '' + return ` +${s}.op-table { + /* 用 separate + border-spacing:0 + box-sizing:border-box,让外框落在表格盒"内"侧: + collapse 模式下外框横跨边缘,被 overlay 的 overflow:hidden 切掉右/下边, + 表现为"需拖一下才看得到边框";separate 方案使四边始终完整可见(设计/打印一致)。 */ + border-collapse: separate; + border-spacing: 0; + box-sizing: border-box; + table-layout: fixed; + /* 字体三件套必须与 DomMeasurer 完全一致,否则分页高度会失真 */ + font-family: ${DEFAULT_FONT_FAMILY}; + font-size: ${TABLE_FONT_SIZE}pt; + line-height: ${DEFAULT_LINE_HEIGHT}; + color: ${TABLE_TEXT_COLOR}; +} + +${s}.op-table td, +${s}.op-table th { + padding: ${mmv(CELL_PADDING_Y)} ${mmv(DEFAULT_CELL_PADDING)}; + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; + font-weight: normal; + text-align: left; + vertical-align: middle; +} + +/* 垂直对齐:vertical-align 不可继承,只能按表加类下发到每个 td */ +${s}.op-table.va-top td { vertical-align: top; } +${s}.op-table.va-middle td { vertical-align: middle; } +${s}.op-table.va-bottom td { vertical-align: bottom; } + +${s}.op-table tr.is-header td { font-weight: bold; } +${s}.op-table tr.is-group td { background: ${TABLE_HEADER_BG}; font-weight: bold; } +${s}.op-table tr.is-subtotal td, +${s}.op-table tr.is-summary td { font-weight: bold; } +${s}.op-table tr.is-striped td { background: ${TABLE_STRIPE_BG}; } +/* 静态行(布局网格正文 / 数据表备注尾行):默认无强调样式, + 单元格视觉以内联样式为准(与设计期 overlay 同一套 resolveCellStyleFor 取值) */ +${s}.op-table tr.is-static td { /* 占位:保持类名与设计期对齐,避免样式遗漏 */ } + +/* ---------- 表格样式预设(Excel 式快速切换;class 由渲染端挂在 table 元素上) ---------- */ +/* 默认(none):仅表头加粗,无任何背景色(含标题行)。无需规则,class 仅作占位。 */ + +/* 表头高亮:表头浅灰底 */ +${s}.op-table.ts-header tr.is-header td { background: ${TABLE_HEADER_BG}; } + +/* 斑马纹(单双变色条纹,浅蓝交替,对比更明显) */ +${s}.op-table.ts-zebra tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-zebra tbody tr:not(.is-header):nth-child(even) td { background: ${TABLE_STRIPE_BG}; } + +/* 斑马纹·蓝(交替行更深蓝) */ +${s}.op-table.ts-zebra-blue tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-zebra-blue tbody tr:not(.is-header):nth-child(even) td { background: ${TS_ZEBRA_BLUE_BG}; } + +/* 斑马纹·灰(中性灰交替) */ +${s}.op-table.ts-zebra-gray tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-zebra-gray tbody tr:not(.is-header):nth-child(even) td { background: ${TS_ZEBRA_GRAY_BG}; } + +/* 绿色表头(绿底白字) */ +${s}.op-table.ts-header-green tr.is-header td { background: ${TABLE_HEADER_GREEN}; color: #ffffff; } + +/* 隔列着色(奇偶列交替底) */ +${s}.op-table.ts-banded-cols tr > *:nth-child(even) { background: ${TS_BANDED_BG}; } + +/* 三段式:表头高亮 + 合计 / 总计行强调底(设计期聚合尾行已对齐 is-subtotal/is-summary 类) */ +${s}.op-table.ts-three-segment tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-three-segment tr.is-subtotal td, +${s}.op-table.ts-three-segment tr.is-summary td { background: #EBF2FF; } + +/* 深底表头:深蓝底白字 */ +${s}.op-table.ts-header-dark tr.is-header td { background: #2F54EB; color: #ffffff; } + +/* 课表:全网格 + 表头淡底高亮(角标斜线由单元格 diagonal 控制) */ +${s}.op-table.ts-timetable tr.is-header td { background: ${TABLE_HEADER_BG}; } + +/* 边框模式:每格画上/左边框,末行补下、末列补右 → 外框落在盒内、不被裁切 */ +${s}.op-table.b-all td { border-top: 0.2mm solid var(--op-table-border); border-left: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-all tr:last-child td { border-bottom: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-all td:last-child { border-right: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-horizontal td { border-top: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-horizontal tr:last-child td { border-bottom: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-outline { border: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-none td { border: 0; } +/* 三线表:顶线 + 表头底线 + 底线,无内部横线、无竖线(建模报告 / 财务报表常用) */ +${s}.op-table.b-three-line { border-top: 0.2mm solid var(--op-table-border); border-bottom: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-three-line tr.is-header:last-child td { border-bottom: 0.2mm solid var(--op-table-border); } +` +} + +export function generateCss(metrics: PageMetrics, options: CssOptions = {}): string { + const screen = options.screen ?? true + const scale = options.scale ?? 1 + const pageBg = options.pageDecoration?.backgroundColor ?? '#ffffff' + const { pageWidth, pageHeight, margin, contentWidth, contentHeight } = metrics + + return ` +/* ============ OpenPrint 渲染样式(自动生成,勿手改) ============ */ +:root { + --op-page-w: ${mmv(pageWidth)}; + --op-page-h: ${mmv(pageHeight)}; + --op-scale: ${screen ? scale : 1}; + --op-table-border: ${TABLE_BORDER_COLOR}; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: ${screen ? '#f0f2f5' : '#ffffff'}; + /* 让底色/斑马纹在打印时不被浏览器"省墨"策略抹掉 */ + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +.op-doc { + display: flex; + flex-direction: column; + align-items: center; + ${screen ? 'gap: 10mm; padding: 10mm 0;' : ''} +} + +/* 缩放外壳:transform 不占布局空间,靠外壳撑出滚动区域 */ +.op-page-wrap { + width: calc(var(--op-page-w) * var(--op-scale)); + height: calc(var(--op-page-h) * var(--op-scale)); + flex: none; +} + +.op-page { + position: relative; + width: var(--op-page-w); + height: var(--op-page-h); + overflow: hidden; + background: ${pageBg}; + transform: scale(var(--op-scale)); + transform-origin: top left; + ${screen ? 'box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);' : ''} +} + +/* 水印装饰层:绝对铺满页面、不拦截事件、置于内容之下 */ +.op-watermark { + position: absolute; + inset: 0; + overflow: hidden; + pointer-events: none; + z-index: 0; +} +.op-watermark.op-wm-single { + display: flex; + align-items: center; + justify-content: center; +} +.op-wm-single > span { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + white-space: nowrap; +} + +/* 页边距内的内容盒;页眉/正文/页脚在其中垂直分区 */ +.op-content { + position: absolute; + left: ${mmv(margin.left)}; + top: ${mmv(margin.top)}; + width: ${mmv(contentWidth)}; + height: ${mmv(contentHeight)}; +} + +.op-section { + position: absolute; + left: 0; + width: 100%; + overflow: hidden; +} +.op-header { top: 0; height: ${mmv(metrics.headerHeight)}; } +.op-body { top: ${mmv(metrics.headerHeight)}; height: ${mmv(metrics.bodyHeight)}; } +.op-footer { top: ${mmv(metrics.headerHeight + metrics.bodyHeight)}; height: ${mmv(metrics.footerHeight)}; } + +/* -------------------------------- 控件 -------------------------------- */ + +.op-node { + position: absolute; + transform-origin: left top; +} + +.op-text { + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; + font-family: ${TEXT_DEFAULT_FONT_FAMILY}; + font-size: ${TEXT_DEFAULT_FONT_SIZE}pt; + line-height: ${TEXT_DEFAULT_LINE_HEIGHT}; + color: ${TEXT_DEFAULT_COLOR}; +} + +.op-richtext { + overflow: hidden; + font-family: ${TEXT_DEFAULT_FONT_FAMILY}; + font-size: ${TEXT_DEFAULT_FONT_SIZE}pt; + line-height: 1.5; + color: ${TEXT_DEFAULT_COLOR}; +} +.op-richtext > *:first-child { margin-top: 0; } +.op-richtext > *:last-child { margin-bottom: 0; } +.op-richtext img { max-width: 100%; } + +.op-image { display: block; } +.op-image > img { display: block; width: 100%; height: 100%; } + +/* 条码 / 二维码:SVG 矢量填满控件框 */ +.op-code > svg { display: block; width: 100%; height: 100%; } + +.op-line > span { + position: absolute; + display: block; +} + +/* 解析失败占位:必须看得见,避免用户拿到"少了一块"的单据而不自知 */ +.op-placeholder { + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed #c0c4cc; + color: #a8abb2; + font-family: ${TEXT_DEFAULT_FONT_FAMILY}; + font-size: 8pt; + overflow: hidden; +} + +/* -------------------------------- 表格 -------------------------------- */ +${tableCss()} +/* ------------------------------ 打印覆盖 ------------------------------ */ + +@page { + size: ${mmv(pageWidth)} ${mmv(pageHeight)}; + margin: 0; +} + +@media print { + html, body { background: #ffffff; } + .op-doc { gap: 0; padding: 0; } + .op-page-wrap { width: auto; height: auto; } + .op-page { + transform: none; + box-shadow: none; + break-after: page; + page-break-after: always; + } + /* 末页不再换页,否则会多打一张空白纸 */ + .op-page-wrap:last-child .op-page { + break-after: auto; + page-break-after: auto; + } +} +${options.extraCss ? `\n/* ---- 自定义样式 ---- */\n${options.extraCss}\n` : ''}`.trim() +} diff --git a/openprint/src/core/renderer-html/html-renderer.ts b/openprint/src/core/renderer-html/html-renderer.ts new file mode 100644 index 0000000..da8dd06 --- /dev/null +++ b/openprint/src/core/renderer-html/html-renderer.ts @@ -0,0 +1,392 @@ +/** + * HTML 渲染器 —— LayoutPage[] → 可打印 HTML + * 真理源:《OpenPrint-设计方案.md》§5.11(打印样式)、§7.3(渲染产物)、§11.2(注入防护) + * + * ## 定位模型 + * + * 引擎已经把一切算成 mm 绝对坐标,渲染器只做「数据 → 字符串」的机械翻译: + * 不做换行、不做测量、不做条件判断。任何在这里出现的排版逻辑都意味着 + * 「测量用一套规则、渲染用另一套」,最终表现为分页错位——所以这里刻意保持"笨"。 + * + * ## 注入防护(§11.2) + * + * - 文本 / 属性值一律 escape + * - richtext 在 data-binder 里已过 DOMPurify,这里直接注入 + * - 条码 SVG 来自 bwip-js / qrcode 生成器,文本已被库转义 + * - 图片 src 过 URL 协议白名单,挡掉 `javascript:` 一类的伪协议 + */ +import type { + ImageControl, + LineControl, + RectControl, + TextControl, +} from '@/types/control' +import type { + LayoutPage, + LayoutResult, + PlacedControl, + PlacedTable, + PlacedNode, + RenderRow, +} from '@/core/layout-engine/types' +import type { WatermarkConfig } from '@/types/template' +import { + generateCss, + mmv, + diagonalBackground, + TEXT_DEFAULT_COLOR, + TEXT_DEFAULT_FONT_FAMILY, + TEXT_DEFAULT_FONT_SIZE, + TEXT_DEFAULT_LINE_HEIGHT, + type CssOptions, +} from './css-generator' +import { MM_TO_PX } from '@/utils/constants' + +/* -------------------------------- 转义 -------------------------------- */ + +const HTML_ESCAPE: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +} + +export function escapeHtml(v: unknown): string { + return String(v ?? '').replace(/[&<>"']/g, (c) => HTML_ESCAPE[c] ?? c) +} + +/** CSS 值消毒:挡掉 `;` `{}` 与 url(javascript:) 之类的样式注入 */ +function cssValue(v: unknown): string { + return String(v ?? '') + .replace(/[;{}<>]/g, '') + .replace(/expression\s*\(/gi, '') + .replace(/url\s*\(\s*['"]?\s*javascript:/gi, 'url(') + .trim() +} + +/** 图片地址协议白名单 */ +function safeSrc(src: string): string { + const s = src.trim() + if (/^(data:image\/|https?:|blob:|\/|\.\/|\.\.\/)/i.test(s)) return s + // 相对路径(不含协议)也放行 + if (!/^[a-z][a-z0-9+.-]*:/i.test(s)) return s + return '' +} + +function styleAttr(pairs: Array): string { + const s = pairs.filter(Boolean).join(';') + return s ? ` style="${escapeHtml(s)}"` : '' +} + +/* ------------------------------ 节点几何 ------------------------------ */ + +function boxStyles(node: PlacedNode): string[] { + const out = [ + `left:${mmv(node.left)}`, + `top:${mmv(node.top)}`, + `width:${mmv(node.width)}`, + `height:${mmv(node.height)}`, + ] + const angle = node.kind === 'control' ? node.angle : undefined + if (angle) out.push(`transform:rotate(${Math.round(angle * 100) / 100}deg)`) + return out +} + +/* -------------------------------- 控件 -------------------------------- */ + +function renderText(node: PlacedControl, text: string): string { + const s = (node.control as TextControl).style ?? {} + const style = [ + ...boxStyles(node), + `font-size:${s.fontSize ?? TEXT_DEFAULT_FONT_SIZE}pt`, + `color:${cssValue(s.fill ?? TEXT_DEFAULT_COLOR)}`, + `font-family:${cssValue(s.fontFamily ?? TEXT_DEFAULT_FONT_FAMILY)}`, + `font-weight:${s.fontWeight === 'bold' ? 'bold' : 'normal'}`, + `font-style:${s.fontStyle === 'italic' ? 'italic' : 'normal'}`, + `text-decoration:${s.textDecoration === 'underline' ? 'underline' : 'none'}`, + `text-align:${s.textAlign ?? 'left'}`, + `line-height:${s.lineHeight ?? TEXT_DEFAULT_LINE_HEIGHT}`, + s.letterSpacing ? `letter-spacing:${s.letterSpacing}pt` : null, + ] + return `
${escapeHtml(text)}
` +} + +function renderImage(node: PlacedControl, src: string): string { + const c = node.control as ImageControl + const url = safeSrc(src) + if (!url) return renderPlaceholder(node, '图片') + const radius = c.cornerRadius ? `border-radius:${c.cornerRadius}px` : null + const imgStyle = [`object-fit:${c.fit ?? 'contain'}`, radius] + return ( + `
` + + `
` + ) +} + +function renderCode(node: PlacedControl, svg: string): string { + // svg 由 bwip-js / qrcode 生成,内容已由库自身转义 + return `
${svg}
` +} + +function renderRichText(node: PlacedControl, html: string): string { + // html 已在 data-binder 中经 DOMPurify 消毒 + return `
${html}
` +} + +function renderShape(node: PlacedControl): string { + const c = node.control + if (c.type === 'line') { + const line = c as LineControl + const stroke = cssValue(line.stroke ?? '#000000') + const sw = line.strokeWidth ?? 1 + const dash = line.strokeDashArray?.length ? 'dashed' : 'solid' + // 协议只存外接框,宽>=高视为水平线(与 PrintLine 的 MVP 约定一致) + const horizontal = node.height <= node.width + const inner = horizontal + ? `left:0;right:0;top:50%;transform:translateY(-50%);border-top:${sw}px ${dash} ${stroke}` + : `top:0;bottom:0;left:50%;transform:translateX(-50%);border-left:${sw}px ${dash} ${stroke}` + return ( + `
` + + `
` + ) + } + + const rect = c as RectControl + const fill = rect.fill && rect.fill !== 'transparent' ? cssValue(rect.fill) : 'transparent' + const sw = rect.strokeWidth ?? 1 + const isCircle = rect.shape === 'circle' + + // 圆角:优先四角独立值,其次统一 cornerRadius,最后无圆角 + let radiusStyle: string | null = null + if (isCircle) { + radiusStyle = 'border-radius:50%' + } else if ( + rect.cornerRadiusTL != null || + rect.cornerRadiusTR != null || + rect.cornerRadiusBR != null || + rect.cornerRadiusBL != null + ) { + const cr = rect.cornerRadius ?? 0 + const tl = rect.cornerRadiusTL ?? cr + const tr = rect.cornerRadiusTR ?? cr + const br = rect.cornerRadiusBR ?? cr + const bl = rect.cornerRadiusBL ?? cr + radiusStyle = `border-radius:${tl}px ${tr}px ${br}px ${bl}px` + } else if (rect.cornerRadius) { + radiusStyle = `border-radius:${rect.cornerRadius}px` + } + + const style = [ + ...boxStyles(node), + `background:${fill}`, + sw > 0 ? `border:${sw}px solid ${cssValue(rect.stroke ?? '#000000')}` : 'border:0', + radiusStyle, + // 虚线:画布侧走 Fabric setLineDash,导出侧用 border-style 近似 + rect.strokeDashArray?.length ? 'border-style:dashed' : null, + ] + return `
` +} + +function renderPlaceholder(node: PlacedControl, label: string): string { + return ( + `
` + + `${escapeHtml(label)}
` + ) +} + +function renderControl(node: PlacedControl): string { + switch (node.content.kind) { + case 'text': + return renderText(node, node.content.text) + case 'html': + return renderRichText(node, node.content.html) + case 'image': + return renderImage(node, node.content.src) + case 'svg': + return renderCode(node, node.content.svg) + case 'shape': + return renderShape(node) + case 'placeholder': + return renderPlaceholder(node, node.content.label) + default: + return '' + } +} + +/* -------------------------------- 表格 -------------------------------- */ + +const ROW_CLASS: Record = { + header: 'is-header', + data: 'is-data', + group: 'is-group', + subtotal: 'is-subtotal', + summary: 'is-summary', + static: 'is-static', +} + +/** + * 输出单元格完整样式。 + * 只写**显式设置过**的属性,其余交给 `tableCss()` 的类规则兜底 —— 设计期 overlay + * (table-design-render.ts)走的是同一套取舍,两端因此像素级一致。 + */ +function renderRow(row: RenderRow, striped: boolean): string { + const cls = [ROW_CLASS[row.kind]] + if (striped && row.kind === 'data' && (row.dataIndex ?? 0) % 2 === 1) cls.push('is-striped') + + const cells = row.cells + .map((cell) => { + const style = [ + `text-align:${cell.align ?? 'left'}`, + cell.background ? `background-color:${cssValue(cell.background)}` : null, + cell.bold !== undefined ? `font-weight:${cell.bold ? 'bold' : 'normal'}` : null, + cell.fontSize ? `font-size:${cell.fontSize}pt` : null, + cell.fontFamily ? `font-family:${cssValue(cell.fontFamily)}` : null, + cell.italic !== undefined ? `font-style:${cell.italic ? 'italic' : 'normal'}` : null, + cell.underline !== undefined + ? `text-decoration:${cell.underline ? 'underline' : 'none'}` + : null, + cell.color ? `color:${cssValue(cell.color)}` : null, + cell.valign ? `vertical-align:${cell.valign}` : null, + cell.diagonal ? diagonalBackground(cell.diagonal) : null, + ] + const span = cell.colSpan && cell.colSpan > 1 ? ` colspan="${cell.colSpan}"` : '' + const rspan = cell.rowSpan && cell.rowSpan > 1 ? ` rowspan="${cell.rowSpan}"` : '' + return `${escapeHtml(cell.text) || '
'}` + }) + .join('') + + return `
${cells}` +} + +function renderTable(node: PlacedTable): string { + const opts = node.control.options ?? {} + const borders = opts.borders ?? 'all' + const striped = opts.striped ?? false + const va = opts.verticalAlign ?? 'middle' + const tableStyle = opts.tableStyle ?? 'none' + + const cols = node.columnWidths + .map((w) => ``) + .join('') + + const body: string[] = [] + for (const row of node.headerRows) body.push(renderRow(row, false)) + for (const row of node.rows) body.push(renderRow(row, striped)) + for (const row of node.footerRows) body.push(renderRow(row, false)) + + const style = [ + `left:${mmv(node.left)}`, + `top:${mmv(node.top)}`, + `width:${mmv(node.width)}`, + ] + + return ( + `
${cols}${body.join('')}
` + ) +} + +function renderNode(node: PlacedNode): string { + return node.kind === 'table' ? renderTable(node) : renderControl(node) +} + +/* --------------------------------- 页 --------------------------------- */ + +function renderSection(cls: string, nodes: PlacedNode[]): string { + if (nodes.length === 0) return `
` + return `
${nodes.map(renderNode).join('')}
` +} + +/** + * 生成水印装饰层 HTML。 + * - 居中单个:flex 居中 + 绝对定位旋转 span + * - 全页平铺:用单个旋转水印瓦片生成的 SVG 作 background-image(repeat) + * 文本已 escape,颜色经 cssValue 消毒。 + */ +function renderWatermark(wm: WatermarkConfig): string { + if (!wm.enabled) return '' + const color = cssValue(wm.color) || '#cccccc' + const size = Math.max(1, wm.fontSize) + const rot = Number.isFinite(wm.rotation) ? wm.rotation : 0 + const text = escapeHtml(wm.text || '') + + if (!wm.tile) { + const span = `${text}` + return `
${span}
` + } + + // 平铺:瓦片尺寸(mm)随字号放大,瓦片内绘制旋转文本 + const spacingMm = Math.max(size * 2.6, 14) + const tilePx = Math.max(1, Math.ceil(spacingMm * (MM_TO_PX))) + const fontSizePx = Math.max(1, size) * MM_TO_PX + const svg = + `` + + `${escapeHtml(wm.text || '')}` + + `` + const uri = `data:image/svg+xml,${encodeURIComponent(svg)}` + const style = `background-image:url('${uri}');background-repeat:repeat;background-size:${mmv(spacingMm)} ${mmv(spacingMm)};` + return `
` +} + +export function renderPage(page: LayoutPage, watermark?: WatermarkConfig | null): string { + const wm = watermark ? renderWatermark(watermark) : '' + return ( + `
` + + wm + + `
` + + renderSection('op-header', page.header) + + renderSection('op-body', page.body) + + renderSection('op-footer', page.footer) + + `
` + ) +} + +/* ------------------------------- 文档装配 ------------------------------ */ + +export interface RenderHtmlOptions extends CssOptions { + /** 生成完整 HTML 文档(含 head/style);false 时只返回 `.op-doc` 片段 */ + fullDocument?: boolean + /** 文档标题(打印时可能作为默认文件名) */ + title?: string + /** 打印后自动关闭窗口(无头/新窗口打印场景用) */ + autoPrint?: boolean +} + +/** 只输出内容片段(宿主页面自行注入样式时用) */ +export function renderFragment(result: LayoutResult, watermark?: WatermarkConfig | null): string { + return `
${result.pages.map((p) => renderPage(p, watermark)).join('')}
` +} + +/** 输出完整可独立打开 / 打印的 HTML 文档 */ +export function renderHtml(result: LayoutResult, options: RenderHtmlOptions = {}): string { + const css = generateCss(result.metrics, options) + const fragment = renderFragment(result, options.pageDecoration?.watermark) + + if (options.fullDocument === false) return fragment + + const autoPrint = options.autoPrint + ? '' + : '' + + return ` + + + + +${escapeHtml(options.title ?? 'OpenPrint')} + + + +${fragment} +${autoPrint} + +` +} + +/** 供预览面板单独取样式用(iframe 里换缩放不必重算 HTML) */ +export function renderStyle(result: LayoutResult, options: CssOptions = {}): string { + return generateCss(result.metrics, options) +} diff --git a/openprint/src/core/renderer-html/index.ts b/openprint/src/core/renderer-html/index.ts new file mode 100644 index 0000000..13c5ea3 --- /dev/null +++ b/openprint/src/core/renderer-html/index.ts @@ -0,0 +1,10 @@ +/** renderer-html 对外出口 */ +export { + escapeHtml, + renderFragment, + renderHtml, + renderPage, + renderStyle, + type RenderHtmlOptions, +} from './html-renderer' +export { generateCss, mmv, type CssOptions } from './css-generator' diff --git a/openprint/src/core/renderer-html/renderer-html.spec.ts b/openprint/src/core/renderer-html/renderer-html.spec.ts new file mode 100644 index 0000000..5394c07 --- /dev/null +++ b/openprint/src/core/renderer-html/renderer-html.spec.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from 'vitest' + +import { layout } from '@/core/layout-engine/pagination-engine' +import { renderHtml, renderPage, renderStyle } from '@/core/renderer-html' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' +import type { RectControl } from '@/types/control' +import type { LayoutPage, PlacedControl } from '@/core/layout-engine/types' + +function rectControl(overrides: Partial): RectControl { + return { + id: 'r1', + type: 'rect', + left: 0, + top: 0, + width: 40, + height: 25, + ...overrides, + } +} + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + const price = 10 + (i % 9) + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price, + amount: qty * price, + } + }) + return { order: { orderNo: 'SO-001' }, customer: { name: '客户' }, items } +} + +describe('renderer-html —— HTML 输出', () => { + const template = createDemoTemplate() + const measurer = createCjkMeasurer() + + it('renderHtml 输出多页文档结构', async () => { + const result = await layout(template, makeData(30), { measurer }) + const html = renderHtml(result, { screen: false }) + expect(html).toContain('op-doc') + expect(html).toContain('op-page') + expect(html).toContain('op-page-wrap') + expect(html).toContain('销售出库单') + }) + + it('renderStyle 注入打印分页样式', async () => { + const result = await layout(template, makeData(10), { measurer }) + const style = renderStyle(result, { screen: false }) + expect(style).toContain('@page') + expect(style).toContain('break-after') + }) + + describe('renderShape —— 形状样式', () => { + /** 用一个只含单个形状控件的页面直接喂给 renderPage 验证 CSS 输出 */ + function renderShapeHtml(c: RectControl): string { + const node: PlacedControl = { + kind: 'control', + id: c.id, + left: c.left, + top: c.top, + width: c.width, + height: c.height, + content: { kind: 'shape' }, + control: c, + } + const page: LayoutPage = { + index: 0, + pageNo: 1, + header: [], + body: [node], + footer: [], + } + return renderPage(page) + } + + it('四角独立圆角输出四值 border-radius', () => { + const html = renderShapeHtml( + rectControl({ cornerRadiusTL: 5, cornerRadiusTR: 10, cornerRadiusBR: 15, cornerRadiusBL: 20 }), + ) + expect(html).toContain('border-radius:5px 10px 15px 20px') + }) + + it('仅设置部分角时,未设置角回落到统一 cornerRadius', () => { + const html = renderShapeHtml(rectControl({ cornerRadius: 8, cornerRadiusTR: 30 })) + // TR=30 覆盖统一值,其余三角=8 + expect(html).toContain('border-radius:8px 30px 8px 8px') + }) + + it('统一 cornerRadius 输出单值 border-radius', () => { + const html = renderShapeHtml(rectControl({ cornerRadius: 8 })) + expect(html).toContain('border-radius:8px') + expect(html).not.toContain('8px 8px 8px 8px') + }) + + it('虚线模式输出 border-style:dashed', () => { + const html = renderShapeHtml(rectControl({ strokeDashArray: [6, 4] })) + expect(html).toContain('border-style:dashed') + }) + + it('圆形(正方形外接框)输出 border-radius:50%', () => { + const html = renderShapeHtml(rectControl({ shape: 'circle', width: 40, height: 40 })) + expect(html).toContain('border-radius:50%') + expect(html).toContain('op-circle') + }) + + it('圆形宽高不等时为椭圆(仍用 border-radius:50%,CSS 自动椭圆化)', () => { + const html = renderShapeHtml(rectControl({ shape: 'circle', width: 60, height: 30 })) + expect(html).toContain('border-radius:50%') + }) + }) +}) diff --git a/openprint/src/core/renderer-html/richtext.spec.ts b/openprint/src/core/renderer-html/richtext.spec.ts new file mode 100644 index 0000000..cd1f431 --- /dev/null +++ b/openprint/src/core/renderer-html/richtext.spec.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' + +import { resolveControlContent } from '@/core/layout-engine/data-binder' +import { renderPage } from '@/core/renderer-html/html-renderer' +import type { RichTextControl } from '@/types/control' +import type { EvalContext, LayoutPage } from '@/core/layout-engine/types' + +/** 构造最小富文本控件 */ +function makeRichText(value: string, id = 'rt-1'): RichTextControl { + return { id, type: 'richtext', left: 0, top: 0, width: 80, height: 24, value } +} + +describe('richtext —— 数据绑定 + 渲染链路', () => { + const ctx: EvalContext = { data: {}, row: undefined, page: 2, pages: 5 } + + it('resolveControlContent 插值 {{page}}/{{pages}} 页码变量', async () => { + const res = await resolveControlContent( + makeRichText('

第 {{page}} 页 / 共 {{pages}} 页

'), + ctx, + ) + expect(res.content.kind).toBe('html') + if (res.content.kind === 'html') { + expect(res.content.html).toContain('第 2 页 / 共 5 页') + } + }) + + // 注:happy-dom 环境 DOMPurify 的标签解析与真实浏览器不同(如

/ + + + + diff --git a/openprint/src/design/canvas/CanvasDesigner.ts b/openprint/src/design/canvas/CanvasDesigner.ts new file mode 100644 index 0000000..a3b9e2c --- /dev/null +++ b/openprint/src/design/canvas/CanvasDesigner.ts @@ -0,0 +1,873 @@ +/** + * CanvasDesigner —— Fabric 画布内核 + * + * 职责:画布初始化 / 页面(A4 白纸)/ 缩放平移 / 控件增删改 / 选中事件 / 序列化。 + * 坐标约定(《标尺与辅助系统》§4.3): + * - 页面左上角 = 画布坐标 (0,0),zoom=1 时 1mm = MM_TO_PX px + * - 居中/平移/缩放全部走 viewportTransform,标尺层直接读 vt[4]/vt[5] + zoom + * - 协议层坐标:body 控件相对"页边距内容区"左上角;zone 子控件相对色带左上角(§5.14.2) + */ +import { Canvas, Rect, Line, Point, FabricObject, Text, Pattern, type TPointerEventInfo } from 'fabric' +import type { AnyControl, ZoneControl } from '@/types/control' +import type { PageSetup, WatermarkConfig } from '@/types/template' +import { MM_TO_PX, ZOOM_MAX, ZOOM_MIN } from '@/utils/constants' +import { toMm } from '@/core/units' +import { createFabricControl, isPrintObject, PrintText, PrintZone, type IPrintObject } from './controls' +import { SmartGuides } from './guides/SmartGuides' +import { PrintTable } from './controls/PrintTable' +import { ensureCells } from '@/core/layout-engine/table-cells' +import { computeGridLayout, hitTestCell } from './table-design-render' +import { computePhysicalPageCount } from './page-geometry' +import { pageGapPx, modelTopToCanvasY, canvasYToModelTop } from './page-gap' +import type { TableControl } from '@/types/control' + +export interface ViewportState { + zoom: number + offsetX: number + offsetY: number +} + +export interface CanvasDesignerEvents { + /** 选中变化(controlId 列表;空数组 = 取消选中) */ + onSelectionChange?: (ids: string[]) => void + /** 控件几何/属性被画布交互修改(拖拽/缩放手柄结束) */ + onObjectModified?: (control: AnyControl) => void + /** 视口变化(缩放/平移),驱动标尺重绘 */ + onViewportChange?: (vp: ViewportState) => void + /** 双击表格单元格进入编辑:返回物化了 cells 的控件与命中行列 */ + onCellEdit?: (info: { controlId: string; control: AnyControl; row: number; col: number }) => void + /** + * 画布变换心跳:对象拖拽/缩放/旋转或视口变化时触发。 + * 表格 HTML overlay 靠它重算定位(不走 store 直连,避免 store ↔ 画布循环依赖)。 + */ + onTransformTick?: () => void + /** 物理页数变化(内容推导或手动分页触发),驱动右侧页面面板刷新 */ + onPageCountChange?: (count: number) => void +} + +type PrintFabricObject = FabricObject & IPrintObject + +export class CanvasDesigner { + canvas!: Canvas + /** 页面尺寸(mm,内部统一换算) */ + pageWidthMm = 210 + pageHeightMm = 297 + marginMm = { top: 10, bottom: 10, left: 10, right: 10 } + + private pageRect!: Rect + /** 第 2..N 页白色背景(与 pageRect 同款,按 stride 堆叠) */ + private extraPageRects: Rect[] = [] + /** 每页内容区参考线(蓝色虚线矩形,非交互、不导出) */ + private pageGuides: Rect[] = [] + /** 每页内容区底部的分页虚线(红色,与渲染 floor(top/bodyH) 落页边界一致) */ + private pageBreaks: Line[] = [] + /** 分页线标签("分页")与每页页码标签("第 N 页") */ + private pageLabels: Text[] = [] + /** 当前文档物理页数 = max(内容推导页数, 手动分页 minPages),最小 1 */ + private pageCount = 1 + /** 手动分页下限:用户「加页/减页」调整;0 = 完全由内容推导 */ + private minPages = 0 + /** + * 页间视觉间距(px,仅设计态):上下两页之间留白,便于分辨页边界。 + * 只作用在画布坐标(模型 mm 保持 flush,导出/渲染不变)。 + */ + private readonly PAGE_GAP_PX = 24 + private smartGuides?: SmartGuides + /** 水印装饰层(非交互、不导出、不进序列化),随页面尺寸/配置重建 */ + private watermarkObjs: FabricObject[] = [] + private lastWatermark?: WatermarkConfig + private events: CanvasDesignerEvents = {} + private spacePanning = false + /** 用户手动缩放后置 true,窗口 resize 不再自动 refit */ + private userZoomed = false + + /** 初始化画布。container 决定画布像素尺寸( ResizeObserver 自适应) */ + init(el: HTMLCanvasElement, container: HTMLElement, events: CanvasDesignerEvents = {}): void { + this.events = events + // Fabric 7 默认 origin 为 center——全局改回 left/top,协议坐标语义保持一致 + FabricObject.ownDefaults.originX = 'left' + FabricObject.ownDefaults.originY = 'top' + this.canvas = new Canvas(el, { + width: container.clientWidth, + height: container.clientHeight, + backgroundColor: '#e8eaed', + preserveObjectStacking: true, + selection: true, + stopContextMenu: true, + fireRightClick: true, + }) + + this.buildPage() + this.fitToContainer(container) + this.bindEvents() + + this.smartGuides = new SmartGuides(this.canvas, () => this.pageRect, () => this.marginMm) + + new ResizeObserver(() => { + this.canvas.setDimensions({ + width: container.clientWidth, + height: container.clientHeight, + }) + // 布局稳定前(面板展开/窗口调整)保持适配;用户手动缩放后不再打扰 + if (!this.userZoomed) this.fitToContainer(container) + this.canvas.requestRenderAll() + this.emitViewport() + }).observe(container) + } + + dispose(): void { + window.removeEventListener('keydown', this.onKeyDown) + window.removeEventListener('keyup', this.onKeyUp) + this.smartGuides?.dispose() + void this.canvas.dispose() + } + + /* -------------------------------- 页面 -------------------------------- */ + + private buildPage(): void { + this.pageRect = new Rect({ + left: 0, + top: 0, + width: this.pageWidthMm * MM_TO_PX, + height: this.pageHeightMm * MM_TO_PX, + fill: this.lastPageBackground || '#ffffff', + selectable: false, + evented: false, + hoverCursor: 'default', + shadow: '0 2px 12px rgba(0,0,0,0.15)', + // 标记:序列化/选中时排除 + excludeFromExport: true, + } as never) + ;(this.pageRect as unknown as { isPageRect: boolean }).isPageRect = true + this.canvas.add(this.pageRect) + this.pageRect.canvas = this.canvas + // 首次构建后按当前内容推导页数并铺好分页线 / 参考线 / 页码 + this.rebuildPages() + } + + /** 记忆最近一次背景色(buildPage 首帧用) */ + private lastPageBackground = '#ffffff' + + /* ------------------------ 页间留白(模型 mm 与画布 px 换算) ------------------------ */ + // 复用 page-gap.ts 纯函数,避免实现与 page-gap.spec.ts 测试逻辑分叉(单一真相来源)。 + + /** 模型 top(mm, flush) 所在页索引 → 该页之前的累计页间距(px) */ + private gapPx(topMm: number): number { + return pageGapPx(topMm, this.pageHeightMm, this.PAGE_GAP_PX) + } + + /** 模型 top(mm, flush) → 画布绝对 y(px):页边距 + 页内偏移 + 累计页间距 */ + private modelTopToY(topMm: number): number { + return modelTopToCanvasY(topMm, this.marginMm.top * MM_TO_PX, this.pageHeightMm, this.PAGE_GAP_PX) + } + + /** 画布绝对 y(px) → 模型 top(mm, flush):反解累计页间距(近页边界最多迭代 2 次收敛) */ + private yToModelTop(y: number): number { + return canvasYToModelTop(y, this.marginMm.top * MM_TO_PX, this.pageHeightMm, this.PAGE_GAP_PX) + } + + /* -------------------------------- 页面 -------------------------------- */ + + /** + * 由正文控件最低底部推导物理页数:floor(maxBottomMm / pageHeightMm) + 1。 + * 与渲染端 layoutStaticOnly 的 `floor(top/bodyH)` 落页口径一致(body 控件按 + * marginTop + top 绝对定位,越过内容区底部即落入下一页)。 + */ + private computeContentPageCount(): number { + let maxBottomMm = 0 + for (const obj of this.getPrintObjects()) { + if (obj instanceof PrintZone) continue + // 模型 mm(flush)口径:先反解页间距再算底部,与渲染/导出一致 + const topMm = this.yToModelTop(obj.top ?? 0) + const bottom = topMm + obj.getScaledHeight() / MM_TO_PX + if (bottom > maxBottomMm) maxBottomMm = bottom + } + return computePhysicalPageCount(maxBottomMm, this.pageHeightMm) + } + + /** 有效物理页数 = max(内容推导, 手动分页下限),最小 1 */ + private computePageCount(): number { + return Math.max(this.minPages, this.computeContentPageCount()) + } + + /** 内容推导页数(不含手动分页),供 store 的「加页」计算 */ + get contentPageCount(): number { + return this.computeContentPageCount() + } + + /** 当前有效物理页数 */ + get effectivePageCount(): number { + return this.pageCount + } + + /** 手动分页下限(0 = 完全由内容推导) */ + get manualPageCount(): number { + return this.minPages + } + + /** 设置手动分页下限并重建页面视觉 */ + setManualPageCount(n: number): void { + this.minPages = Math.max(0, Math.floor(n) || 0) + this.rebuildPages() + } + + /** + * 依据正文控件布局重建多页视觉:第 2..N 页白色背景、每页内容区参考线、 + * 内容区底部的分页虚线 + 「分页」标签、每页「第 N 页」页码标签。 + * 背景/参考线/分页线全部排在控件下方(控件 bringToFront),避免遮挡正文。 + */ + private rebuildPages(): void { + const count = Math.max(1, this.computePageCount()) + const changed = count !== this.pageCount + this.pageCount = count + const stale = [ + ...this.extraPageRects, + ...this.pageGuides, + ...this.pageBreaks, + ...this.pageLabels, + ] + for (const o of stale) this.canvas.remove(o) + this.extraPageRects = [] + this.pageGuides = [] + this.pageBreaks = [] + this.pageLabels = [] + + const pageW = this.pageWidthMm * MM_TO_PX + const pageH = this.pageHeightMm * MM_TO_PX + const stride = pageH + this.PAGE_GAP_PX + const ml = this.marginMm.left * MM_TO_PX + const mt = this.marginMm.top * MM_TO_PX + const mr = this.marginMm.right * MM_TO_PX + const mb = this.marginMm.bottom * MM_TO_PX + const bodyH = Math.max(1, pageH - mt - mb) + const contentW = Math.max(1, pageW - ml - mr) + const bg = this.lastPageBackground || '#ffffff' + + // 第 2..N 页背景(按 stride 堆叠,带阴影) + for (let i = 1; i < count; i++) { + const r = new Rect({ + left: 0, + top: i * stride, + width: pageW, + height: pageH, + fill: bg, + selectable: false, + evented: false, + hoverCursor: 'default', + shadow: '0 2px 12px rgba(0,0,0,0.15)', + excludeFromExport: true, + objectCaching: false, + } as never) + ;(r as unknown as { isPageRect: boolean }).isPageRect = true + this.canvas.add(r) + this.extraPageRects.push(r) + } + + // 每页内容区参考线(蓝色虚线) + for (let i = 0; i < count; i++) { + const g = new Rect({ + left: ml, + top: i * stride + mt, + width: contentW, + height: bodyH, + fill: '', + stroke: '#1677ff', + strokeWidth: 1, + strokeDashArray: [4, 3], + selectable: false, + evented: false, + hoverCursor: 'default', + excludeFromExport: true, + objectCaching: false, + } as never) + ;(g as unknown as { isMarginGuide: boolean }).isMarginGuide = true + this.canvas.add(g) + this.pageGuides.push(g) + } + + // 每页内容区底部的分页虚线(最后一页不画),并标注「分页」 + for (let i = 0; i < count - 1; i++) { + const y = i * stride + mt + bodyH + const line = new Line([ml, y, ml + contentW, y], { + stroke: '#f5222d', + strokeWidth: 1, + strokeDashArray: [6, 4], + selectable: false, + evented: false, + excludeFromExport: true, + objectCaching: false, + } as never) + this.canvas.add(line) + this.pageBreaks.push(line) + const tag = new Text('分页', { + left: ml + 4, + top: y - 13, + fontSize: 9, + fill: '#f5222d', + selectable: false, + evented: false, + excludeFromExport: true, + } as never) + this.canvas.add(tag) + this.pageLabels.push(tag) + } + + // 每页页码标签(左上角) + for (let i = 0; i < count; i++) { + const lbl = new Text(`第 ${i + 1} 页`, { + left: ml + 4, + top: i * stride + 4, + fontSize: 9, + fill: '#999999', + selectable: false, + evented: false, + excludeFromExport: true, + } as never) + this.canvas.add(lbl) + this.pageLabels.push(lbl) + } + + // 【层级修复】背景/参考线/分页线全部排在正文控件之下,杜绝「第二页盖住控件」 + for (const obj of this.getPrintObjects()) this.canvas.bringObjectToFront(obj) + + this.canvas.requestRenderAll() + if (changed) this.events.onPageCountChange?.(count) + } + + /** 正文控件布局变化后由外部调用,重算页数与分页线 */ + refreshPages(): void { + this.rebuildPages() + } + + /** 显示/隐藏页边距参考线(影响全部页) */ + setMarginGuidesVisible(visible: boolean): void { + for (const g of this.pageGuides) g.visible = visible + this.canvas.requestRenderAll() + } + + /** + * 应用页面设置。 + * 【约定】PageSetup.width/height 一律是**物理尺寸**(实际打印宽高,横向时宽>高), + * orientation 仅是派生标志(UI 用)。方向切换由面板在改 width/height 时完成, + * 这里**不再**按 orientation 二次交换 —— 否则横向页会被换回纵向(旧 bug)。 + * 渲染端 computePageMetrics / 导出 export-pdf 同口径(w>h 判定),全链路所见即所得。 + */ + setPage(setup: PageSetup): void { + this.pageWidthMm = toMm(setup.width, setup.unit) + this.pageHeightMm = toMm(setup.height, setup.unit) + this.marginMm = { + top: toMm(setup.margin.top, setup.unit), + bottom: toMm(setup.margin.bottom, setup.unit), + left: toMm(setup.margin.left, setup.unit), + right: toMm(setup.margin.right, setup.unit), + } + this.minPages = setup.minPages ?? 0 + this.pageRect.set({ + width: this.pageWidthMm * MM_TO_PX, + height: this.pageHeightMm * MM_TO_PX, + fill: this.lastPageBackground || '#ffffff', + }) + // 页面尺寸变化:重建全部页视觉(背景/参考线/分页线/页码)后再注入水印 + this.rebuildPages() + for (const z of this.getZones()) z.relayout(this.pageWidthMm, this.pageHeightMm) + this.syncWatermark() + this.canvas.requestRenderAll() + } + + /** 设置画布页面背景色(不导出、随模板持久化);同步填充所有页 */ + setPageBackground(color: string): void { + this.lastPageBackground = color || '#ffffff' + this.pageRect.set({ fill: this.lastPageBackground }) + for (const r of this.extraPageRects) r.set({ fill: this.lastPageBackground }) + this.canvas.requestRenderAll() + } + + /** 设置画布水印(enabled=false 时清空;tile=全页平铺 / 否则居中单个) */ + setWatermark(cfg: WatermarkConfig | undefined): void { + this.lastWatermark = cfg && cfg.enabled ? cfg : undefined + this.syncWatermark() + } + + /** 重建水印装饰层:先清旧对象,再按配置绘制 */ + private syncWatermark(): void { + for (const o of this.watermarkObjs) this.canvas.remove(o) + this.watermarkObjs = [] + const cfg = this.lastWatermark + if (!cfg) { + this.canvas.requestRenderAll() + return + } + + const pageWpx = this.pageWidthMm * MM_TO_PX + const pageHpx = this.pageHeightMm * MM_TO_PX + const fontSizePx = Math.max(1, cfg.fontSize) * MM_TO_PX + const fontFamily = '"Source Han Sans CN", "PingFang SC", sans-serif' + + if (cfg.tile) { + // 全页平铺:用离屏 canvas 生成单个旋转水印瓦片,作为 pageRect 之上的 Pattern + const spacingMm = Math.max(cfg.fontSize * 2.6, 14) + const tilePx = Math.max(1, Math.ceil(spacingMm * MM_TO_PX)) + const tile = document.createElement('canvas') + tile.width = tilePx + tile.height = tilePx + const ctx = tile.getContext('2d') + if (ctx) { + ctx.clearRect(0, 0, tilePx, tilePx) + ctx.translate(tilePx / 2, tilePx / 2) + ctx.rotate((cfg.rotation * Math.PI) / 180) + ctx.fillStyle = cfg.color + ctx.font = `${fontSizePx}px ${fontFamily}` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(cfg.text || '', 0, 0) + } + const pattern = new Pattern({ source: tile, repeat: 'repeat' }) + const rect = new Rect({ + left: 0, + top: 0, + width: pageWpx, + height: pageHpx, + fill: pattern as unknown as string, + selectable: false, + evented: false, + excludeFromExport: true, + objectCaching: false, + } as never) + ;(rect as unknown as { isWatermark: boolean }).isWatermark = true + this.canvas.insertAt(1, rect) + this.watermarkObjs.push(rect) + } else { + // 居中单个 + const text = new Text(cfg.text || ' ', { + left: pageWpx / 2, + top: pageHpx / 2, + originX: 'center', + originY: 'center', + fontSize: fontSizePx, + fill: cfg.color, + fontFamily, + angle: cfg.rotation, + selectable: false, + evented: false, + excludeFromExport: true, + } as never) + ;(text as unknown as { isWatermark: boolean }).isWatermark = true + this.canvas.insertAt(1, text) + this.watermarkObjs.push(text) + } + + this.canvas.requestRenderAll() + } + + /** 页边距内容区左上角(画布 px)—— body 控件协议坐标的原点 */ + get contentOriginPx(): { x: number; y: number } { + return { x: this.marginMm.left * MM_TO_PX, y: this.marginMm.top * MM_TO_PX } + } + + /* ------------------------------- 控件 CRUD ------------------------------ */ + + getPrintObjects(): PrintFabricObject[] { + return this.canvas.getObjects().filter(isPrintObject) as PrintFabricObject[] + } + + getControlById(id: string): PrintFabricObject | undefined { + return this.getPrintObjects().find((o) => o.controlId === id) + } + + getZones(): PrintZone[] { + return this.getPrintObjects().filter((o): o is PrintZone => o instanceof PrintZone) + } + + hasZone(zone: 'header' | 'footer'): boolean { + return this.getZones().some((z) => z.zone === zone) + } + + addControl( + control: AnyControl, + opts: { select?: boolean; zoneHostId?: string } = {}, + ): void { + const obj = createFabricControl(control, { + widthMm: this.pageWidthMm, + heightMm: this.pageHeightMm, + }) + // 协议坐标(相对内容区/所属色带)→ 画布绝对坐标 + this.applySectionOrigin(obj, control, opts.zoneHostId) + // 镜像「常驻辅助线」开关到 Fabric 对象,供 SmartGuides 读取 + ;(obj as unknown as { showGuides: boolean }).showGuides = control.showGuides ?? false + this.canvas.add(obj) + if (opts.select ?? true) this.canvas.setActiveObject(obj) + this.refreshPages() + this.canvas.requestRenderAll() + } + + /** 清空全部控件(保留页面矩形),加载模板前调用 */ + clearControls(): void { + for (const obj of this.getPrintObjects()) this.canvas.remove(obj) + this.canvas.discardActiveObject() + this.refreshPages() + this.canvas.requestRenderAll() + } + + /** body 控件加内容区偏移(含页间距);zone 子控件加所属色带偏移(MVP:按几何中心归属) */ + private applySectionOrigin( + obj: PrintFabricObject, + control: AnyControl, + zoneHostId?: string, + ): void { + if (control.type === 'zone') return + if (zoneHostId) { + const host = this.getControlById(zoneHostId) + if (host) { + // zone 子控件:相对色带左上角,无页间距 + obj.set({ + left: (Number.isFinite(obj.left) ? obj.left! : 0) + (host.left ?? 0), + top: (Number.isFinite(obj.top) ? obj.top! : 0) + (host.top ?? 0), + }) + } + } else { + // body 控件:obj.top 此刻是 mm→px(相对页顶),modelTopToY 叠加内容区偏移与页间距 + obj.set({ + left: (Number.isFinite(obj.left) ? obj.left! : 0) + this.contentOriginPx.x, + top: this.modelTopToY((obj.top ?? 0) / MM_TO_PX), + }) + } + obj.setCoords() + } + + updateControl(control: AnyControl): void { + const obj = this.getControlById(control.id) + if (!obj) return + if (control.type !== 'zone') { + // 几何同步:协议 mm(相对内容区/所属色带)→ 画布绝对 px(含页间距) + const origin = this.originFor(obj) + obj.set({ + left: origin.x + control.left * MM_TO_PX, + top: origin.y + control.top * MM_TO_PX + (obj instanceof PrintZone ? 0 : this.gapPx(control.top)), + angle: control.angle ?? 0, + }) + if (obj instanceof PrintText) { + // 文本框:直接改宽度重排文字,不缩放字形 + obj.set({ width: control.width * MM_TO_PX, scaleX: 1, scaleY: 1 }) + } else if (obj.width && obj.height) { + // 其余控件:按目标尺寸等比缩放(Group/Line/位图均适用) + obj.set({ + scaleX: (control.width * MM_TO_PX) / obj.width, + scaleY: (control.height * MM_TO_PX) / obj.height, + }) + } + obj.setCoords() + } + ;(obj as IPrintObject).applyControlProps(control) + // 同步「常驻辅助线」开关到 Fabric 对象(控制面板实时切换即生效) + ;(obj as unknown as { showGuides: boolean }).showGuides = control.showGuides ?? false + this.refreshPages() + this.canvas.requestRenderAll() + } + + /** 几何中心落在哪个色带内(§5.14.2 子控件坐标相对色带左上角) */ + private hostZoneOf(obj: PrintFabricObject): PrintZone | undefined { + if (obj instanceof PrintZone) return undefined + const center = obj.getCenterPoint() + return this.getZones().find((z) => { + const b = z.getBoundingRect() + return ( + center.x >= b.left && center.x <= b.left + b.width && center.y >= b.top && center.y <= b.top + b.height + ) + }) + } + + /** 对象协议坐标原点:zone 子控件用色带左上角,其余用页边距内容区左上角 */ + private originFor(obj: PrintFabricObject): { x: number; y: number } { + const host = this.hostZoneOf(obj) + if (host) return { x: host.left ?? 0, y: host.top ?? 0 } + return this.contentOriginPx + } + + /** + * 读取 Fabric 对象为**协议控件**。 + * + * 【重要】`obj.toControl()` 返回的 left/top 是 **Fabric 绝对画布坐标**, + * 而协议层 / store 模型一律是「相对内容区(或所属色带)左上角」的 mm。 + * 任何要把画布状态写回 store 的路径都必须走本方法归一化,否则 `updateControl()` + * 会再叠加一次 origin,导致控件每次操作都向右下平移一个页边距。 + */ + readControl(obj: PrintFabricObject): AnyControl { + const control = obj.toControl() + if (obj instanceof PrintZone || control.type === 'zone') return control + const host = this.hostZoneOf(obj) + const origin = host ? { x: host.left ?? 0, y: host.top ?? 0 } : this.contentOriginPx + const abs = (v: number | undefined): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0) + const rel = (v: number, o: number): number => Math.round(((v - o) / MM_TO_PX) * 100) / 100 + const left = rel(abs(obj.left), origin.x) + // body 控件反解页间距(zone 子控件相对色带,间距恒为 0) + const top = host ? rel(abs(obj.top), origin.y) : Math.round(this.yToModelTop(abs(obj.top)) * 100) / 100 + // 色带子控件不允许溢出到色带上方/左侧 + control.left = host ? Math.max(0, left) : left + control.top = host ? Math.max(0, top) : top + return control + } + + removeControl(id: string): void { + const obj = this.getControlById(id) + if (!obj) return + this.canvas.remove(obj) + this.canvas.discardActiveObject() + this.refreshPages() + this.canvas.requestRenderAll() + } + + setActiveControl(id: string | null): void { + if (!id) { + this.canvas.discardActiveObject() + } else { + const obj = this.getControlById(id) + if (obj) this.canvas.setActiveObject(obj) + } + this.canvas.requestRenderAll() + } + + /* ------------------------------- 序列化 -------------------------------- */ + + /** + * 导出当前画布为协议组件列表。 + * zone 子控件归属判定:几何中心落在色带内(§5.14.2 子组件坐标相对色带左上角)。 + */ + serialize(): { body: AnyControl[]; zones: ZoneControl[] } { + const zones = this.getZones() + const zoneControls: ZoneControl[] = zones.map((z) => z.toControl()) + const body: AnyControl[] = [] + + for (const obj of this.getPrintObjects()) { + if (obj instanceof PrintZone) continue + // readControl 已把几何归一化到「相对色带 / 相对内容区」 + const control = this.readControl(obj) + const host = this.hostZoneOf(obj) + if (host) { + const zc = zoneControls.find((z) => z.zone === host.zone) + zc?.children.push(control) + } else { + body.push(control) + } + } + return { body, zones: zoneControls } + } + + /* ------------------------------- 缩放平移 ------------------------------- */ + + get zoom(): number { + return this.canvas?.getZoom() ?? 1 + } + + setZoom(zoom: number, center?: Point): void { + this.userZoomed = true + const z = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom)) + const point = + center ?? new Point(this.canvas.getWidth() / 2, this.canvas.getHeight() / 2) + this.canvas.zoomToPoint(point, z) + this.emitViewport() + } + + zoomIn(): void { + this.setZoom(this.zoom + 0.25) + } + + zoomOut(): void { + this.setZoom(this.zoom - 0.25) + } + + /** 适配容器:按页宽适配(上限 200%),文档可整页容纳则垂直居中,否则顶部对齐以便向下滚动翻页 */ + fitToContainer(container: HTMLElement): void { + const pageW = this.pageWidthMm * MM_TO_PX + const stride = this.pageHeightMm * MM_TO_PX + this.PAGE_GAP_PX + const docH = this.pageCount * stride + const zoom = Math.min((container.clientWidth - 96) / pageW, 2) + const vt = this.canvas.viewportTransform + vt[0] = vt[3] = zoom + vt[4] = (container.clientWidth - pageW * zoom) / 2 + const usedH = docH * zoom + vt[5] = usedH <= container.clientHeight - 96 ? Math.max(48, (container.clientHeight - usedH) / 2) : 48 + this.canvas.setViewportTransform(vt) + this.canvas.requestRenderAll() + this.emitViewport() + } + + /** 滚动视口使第 index 页(0 起)顶部对齐到视口上方(顶部留 48px) */ + scrollToPage(index: number): void { + const stride = (this.pageHeightMm * MM_TO_PX + this.PAGE_GAP_PX) * this.zoom + const vt = this.canvas.viewportTransform + const viewH = this.canvas.getHeight() + const docH = this.pageCount * stride + const topY = Math.max(0, Math.min(index, this.pageCount - 1)) * stride + let y = 48 - topY + // 约束:不滚出文档顶部 / 底部 + const maxY = 48 + const minY = Math.min(maxY, viewH - docH - 48) + vt[5] = Math.max(Math.min(y, maxY), minY) + this.canvas.setViewportTransform(vt) + this.canvas.requestRenderAll() + this.emitViewport() + } + + getViewportState(): ViewportState { + const vt = this.canvas.viewportTransform + return { zoom: this.zoom, offsetX: vt[4] ?? 0, offsetY: vt[5] ?? 0 } + } + + private emitViewport(): void { + this.events.onViewportChange?.(this.getViewportState()) + // 视口变化同样需要驱动表格 HTML overlay 重定位 + this.events.onTransformTick?.() + } + + /* -------------------------------- 事件 -------------------------------- */ + + private bindEvents(): void { + const c = this.canvas + + const emitSelection = () => { + const active = c.getActiveObjects().filter(isPrintObject) as PrintFabricObject[] + this.events.onSelectionChange?.(active.map((o) => o.controlId)) + } + c.on('selection:created', emitSelection) + c.on('selection:updated', emitSelection) + c.on('selection:cleared', () => this.events.onSelectionChange?.([])) + + // 对象变换 / 视口变化 → 驱动表格 HTML overlay 重定位 + const bump = () => this.events.onTransformTick?.() + c.on('object:moving', bump) + c.on('object:scaling', bump) + c.on('object:rotating', bump) + c.on('object:modified', bump) + c.on('object:added', bump) + c.on('object:removed', bump) + + // 双击单元格:进入原生 contenteditable 编辑(方案 A) + c.on('mouse:dblclick', (opt) => { + const target = opt.target + if (!(target instanceof PrintTable)) return + // 必须用 readControl(相对坐标),直接 toControl() 会把绝对坐标写进 store 导致漂移 + const control = this.readControl(target) as TableControl + const ensured = ensureCells(control) + // 场景坐标 → 控件局部坐标(先平移到左上角原点,再反向旋转,最后归一化) + const dx = opt.scenePoint.x - (target.left ?? 0) + const dy = opt.scenePoint.y - (target.top ?? 0) + const rad = (-(target.angle ?? 0) * Math.PI) / 180 + const localX = dx * Math.cos(rad) - dy * Math.sin(rad) + const localY = dx * Math.sin(rad) + dy * Math.cos(rad) + const hit = hitTestCell( + computeGridLayout(ensured), + localX / Math.max(1e-6, target.getScaledWidth()), + localY / Math.max(1e-6, target.getScaledHeight()), + ) + if (!hit) return + this.events.onCellEdit?.({ controlId: target.controlId, control: ensured, row: hit.row, col: hit.col }) + }) + + c.on('object:modified', (e) => { + const obj = e.target + if (!obj || !isPrintObject(obj)) return + if (obj instanceof PrintZone) { + // zone 只允许改高度:把位置吸附回页顶/页底 + obj.relayout(this.pageWidthMm, this.pageHeightMm) + } + // 位图类控件缩放结束后按新尺寸重渲染,保证清晰 + const regenerable = obj as Partial<{ regenerate: () => void }> + if (typeof regenerable.regenerate === 'function' && !(obj instanceof PrintZone)) { + const scaledW = obj.getScaledWidth() + const scaledH = obj.getScaledHeight() + obj.set({ width: scaledW, height: scaledH, scaleX: 1, scaleY: 1 }) + regenerable.regenerate() + } + this.events.onObjectModified?.(this.readControl(obj)) + // 移动/缩放结束后正文布局可能跨页,刷新页数与分页线 + this.refreshPages() + }) + + // Ctrl/Cmd + 滚轮缩放;普通滚轮 = 垂直平移;Shift+滚轮 = 水平平移 + c.on('mouse:wheel', (opt: TPointerEventInfo) => { + const e = opt.e + e.preventDefault() + e.stopPropagation() + const vt = c.viewportTransform + if (e.ctrlKey || e.metaKey) { + const delta = e.deltaY + const zoom = this.zoom * 0.999 ** delta + this.setZoom(zoom, new Point(e.offsetX, e.offsetY)) + } else { + if (e.shiftKey) { + vt[4] = (vt[4] ?? 0) - e.deltaY + } else { + vt[5] = (vt[5] ?? 0) - e.deltaY + vt[4] = (vt[4] ?? 0) - e.deltaX + } + c.setViewportTransform(vt) + c.requestRenderAll() + this.emitViewport() + } + }) + + // 空格 + 拖拽 = 平移 + c.on('mouse:down', (opt) => { + if (this.spacePanning) { + const e = opt.e as MouseEvent + c.selection = false + c.defaultCursor = 'grab' + let lastX = e.clientX + let lastY = e.clientY + const move = (ev: MouseEvent) => { + const vt = c.viewportTransform + vt[4] = (vt[4] ?? 0) + (ev.clientX - lastX) + vt[5] = (vt[5] ?? 0) + (ev.clientY - lastY) + lastX = ev.clientX + lastY = ev.clientY + c.setViewportTransform(vt) + c.requestRenderAll() + this.emitViewport() + } + const up = () => { + window.removeEventListener('mousemove', move) + window.removeEventListener('mouseup', up) + c.selection = true + c.defaultCursor = 'default' + } + window.addEventListener('mousemove', move) + window.addEventListener('mouseup', up) + } + }) + + window.addEventListener('keydown', this.onKeyDown) + window.addEventListener('keyup', this.onKeyUp) + } + + private onKeyDown = (e: KeyboardEvent): void => { + if (e.code === 'Space' && !this.isEditingText()) { + this.spacePanning = true + e.preventDefault() + } + } + + private onKeyUp = (e: KeyboardEvent): void => { + if (e.code === 'Space') this.spacePanning = false + } + + private isEditingText(): boolean { + const active = this.canvas.getActiveObject() as unknown as { isEditing?: boolean } | undefined + return !!active?.isEditing + } + + /* ----------------------------- 网格控制 ------------------------------ */ + + setGridSize(sizeMm: number): void { + if (this.smartGuides) this.smartGuides.config.gridSizeMm = sizeMm + } + + setGridColor(color: string): void { + if (this.smartGuides) this.smartGuides.config.gridColor = color + this.canvas.requestRenderAll() + } + + setGridVisible(visible: boolean): void { + if (this.smartGuides) this.smartGuides.config.gridVisible = visible + this.canvas.requestRenderAll() + } +} diff --git a/openprint/src/design/canvas/CanvasStage.vue b/openprint/src/design/canvas/CanvasStage.vue new file mode 100644 index 0000000..0edf1ea --- /dev/null +++ b/openprint/src/design/canvas/CanvasStage.vue @@ -0,0 +1,109 @@ + + + diff --git a/openprint/src/design/canvas/CellToolbar.vue b/openprint/src/design/canvas/CellToolbar.vue new file mode 100644 index 0000000..ab876ba --- /dev/null +++ b/openprint/src/design/canvas/CellToolbar.vue @@ -0,0 +1,620 @@ + + + + + diff --git a/openprint/src/design/canvas/ChartViewLayer.vue b/openprint/src/design/canvas/ChartViewLayer.vue new file mode 100644 index 0000000..b6f28f6 --- /dev/null +++ b/openprint/src/design/canvas/ChartViewLayer.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/openprint/src/design/canvas/TableViewLayer.vue b/openprint/src/design/canvas/TableViewLayer.vue new file mode 100644 index 0000000..d91f946 --- /dev/null +++ b/openprint/src/design/canvas/TableViewLayer.vue @@ -0,0 +1,498 @@ + + + + + diff --git a/openprint/src/design/canvas/barcode-draw.ts b/openprint/src/design/canvas/barcode-draw.ts new file mode 100644 index 0000000..9e20073 --- /dev/null +++ b/openprint/src/design/canvas/barcode-draw.ts @@ -0,0 +1,78 @@ +/** + * @bwip-js/generic → Canvas 2D 适配器 + * + * 改用官方 toSVG() 生成条码(含人眼可读数字的人体化矢量路径,无字体依赖), + * 再栅格化到离屏 canvas 供 Fabric 显示。 + * 原先的手写 CanvasDrawing 适配器对人眼可读文字的测量/绘制有缺陷, + * 导致画布上条码下方的数字不显示(预览走 toSVG 正常,故仅画布有此问题)。 + */ +import * as BwipJs from '@bwip-js/generic' + +export interface BarcodeDrawOptions { + /** bwip-js bcid,如 code128 / ean13 */ + format?: string + text: string + showText?: boolean + /** 条码条本身高度(mm)。bwip-js 的 height 选项只管条码条,不含文字行 */ + barHeightMM?: number + /** 上下留白(mm),给文字行呼吸空间,避免贴边 */ + paddingMM?: number +} + +/** 把 bwip-js 的 SVG 字符串栅格化到离屏 canvas(人眼可读数字以矢量路径渲染,天然可见) */ +function svgToCanvas(svg: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + img.onload = () => { + const w = img.naturalWidth || parseSvgSize(svg, 'width') + const h = img.naturalHeight || parseSvgSize(svg, 'height') + const canvas = document.createElement('canvas') + canvas.width = Math.max(1, w) + canvas.height = Math.max(1, h) + const ctx = canvas.getContext('2d') + if (!ctx) return reject(new Error('no 2d context')) + ctx.drawImage(img, 0, 0, canvas.width, canvas.height) + resolve(canvas) + } + img.onerror = () => reject(new Error('barcode svg decode failed')) + img.src = url + }) +} + +/** SVG 未带 width/height 时从 viewBox 兜底解析 */ +function parseSvgSize(svg: string, attr: 'width' | 'height'): number { + const vb = /viewBox="0 0 (\d+) (\d+)"/.exec(svg) + if (vb) return Number(vb[attr === 'width' ? 1 : 2]) || 1 + const m = new RegExp(`${attr}="(\\d+)"`).exec(svg) + return m ? Number(m[1]) || 1 : 1 +} + +/** + * 渲染条码到离屏 canvas(失败返回 null,由调用方降级为占位) + * + * 关键:barHeightMM + paddingMM 让 bwip-js 直接按目标几何渲染, + * canvas 自然尺寸 ≈ 控件尺寸,调用方 scaleY ≈ 1,文字行不再被压扁。 + * (原先只用 scale:3 生成自然尺寸位图,再被调用方 Y 方向压缩,导致文字看不见。) + */ +export async function drawBarcode(options: BarcodeDrawOptions): Promise { + try { + const barHeight = options.barHeightMM ?? 10 + const padding = options.paddingMM ?? 2 + const svg = BwipJs.toSVG({ + bcid: (options.format ?? 'code128').toLowerCase(), + text: options.text || '0123456789', + scale: 2, + height: barHeight, + paddingtop: padding, + paddingbottom: padding, + includetext: options.showText ?? false, + textxalign: 'center', + // BWIPP textsize(单位 point,默认 9),调到 12 让人眼可读数字更醒目 + textsize: 12, + }) + return await svgToCanvas(svg) + } catch { + return null + } +} diff --git a/openprint/src/design/canvas/controls/PrintBarcode.ts b/openprint/src/design/canvas/controls/PrintBarcode.ts new file mode 100644 index 0000000..497886a --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintBarcode.ts @@ -0,0 +1,105 @@ +/** + * PrintBarcode —— 条形码控件(§5.7,bwip-js 离屏渲染) + * 设计期用绑定路径占位文本渲染,运行期由渲染引擎注入真实值。 + */ +import { FabricImage } from 'fabric' +import type { BarcodeControl } from '@/types/control' +import { mm, px, readBaseGeometry, type IPrintObject } from './PrintObject' +import { drawBarcode } from '../barcode-draw' + +export class PrintBarcode extends FabricImage implements IPrintObject { + controlId: string + controlType = 'barcode' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + binding?: string + textValue?: string + format = 'CODE128' + showText = true + + constructor(control: BarcodeControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.binding = control.binding + this.textValue = control.value + this.format = control.format ?? 'CODE128' + this.showText = control.showText ?? true + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + // 初始尺寸(未缩放),regenerate 会按它换算 scale + this.set({ width: mm(control.width), height: mm(control.height) }) + void this.regenerate() + } + + /** 重新渲染条码位图,并按控件宽高缩放显示 */ + async regenerate(): Promise { + const w = this.width || 1 + const h = this.height || 1 + const text = this.textValue ?? (this.binding ? `{{${this.binding}}}` : '0123456789') + // 绑定占位符含中文/大括号无法编码,设计期用示例码代替 + const encodable = /[{}\u4e00-\u9fa5]/.test(text) ? 'DEMO123456' : text + // 按控件几何反算条码条高度(mm):条码条占 60%,剩 40% 留给文字行(textsize=12)+ 上下留白。 + // 这样 bwip-js 直接按目标尺寸渲染,canvas 自然高度 ≈ 控件高度,scaleY ≈ 1,文字不被压扁。 + const controlHeightMM = px(h) + const barHeightMM = Math.max(2, controlHeightMM * 0.6) + const paddingMM = Math.max(0.5, controlHeightMM * 0.04) + const canvas = await drawBarcode({ + format: this.format, + text: encodable, + showText: this.showText, + barHeightMM, + paddingMM, + }) + if (canvas) { + this.setElement(canvas) + // 宽度独立缩放填满控件;高度按 canvas 自然尺寸缩放(已接近控件高度,scaleY ≈ 1)。 + // 不再强行 Y 方向压缩,避免文字行被挤没。 + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + } + this.setCoords() + this.canvas?.requestRenderAll() + } + + toControl(): BarcodeControl { + return { + ...readBaseGeometry(this), + type: 'barcode', + binding: this.binding, + value: this.textValue, + format: this.format, + showText: this.showText, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: BarcodeControl): void { + this.binding = control.binding + this.textValue = control.value + this.format = control.format ?? 'CODE128' + this.showText = control.showText ?? true + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + void this.regenerate() + } +} diff --git a/openprint/src/design/canvas/controls/PrintChart.ts b/openprint/src/design/canvas/controls/PrintChart.ts new file mode 100644 index 0000000..ef6ee64 --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintChart.ts @@ -0,0 +1,113 @@ +/** + * PrintChart —— 图表控件(《设计方案》图表控件提案) + * + * 与 PrintTable 同属【方案 A】:**视觉**完全交给 HTML overlay(ChartViewLayer.vue), + * 本 Fabric 对象退化为「透明宿主」——只承担选中 / 拖拽 / 缩放手柄 / 层级 / 序列化, + * 位图上仅画一圈极淡虚线轮廓作为兜底。SVG 矢量由 chartkit 生成, + * 设计期与运行期导出共用同一份产物(设计即打印)。 + */ +import { FabricImage } from 'fabric' +import type { ChartControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +/** overlay 兜底轮廓色(极淡,正常情况下被 overlay 的真实图表完全覆盖) */ +const GHOST_COLOR = 'rgba(120,130,145,0.35)' + +export class PrintChart extends FabricImage implements IPrintObject { + controlId: string + controlType = 'chart' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + kind: ChartControl['kind'] + categories: string[] + series: ChartControl['series'] + options: ChartControl['options'] + + /** true = 视觉交给 HTML overlay,位图只画兜底轮廓 */ + htmlPreview = true + + constructor(control: ChartControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.kind = control.kind + this.categories = control.categories ?? [] + this.series = control.series ?? [] + this.options = control.options + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ width: mm(control.width), height: mm(control.height) }) + this.regenerate() + } + + /** 重绘设计期预览(2x 分辨率保证缩放清晰) */ + regenerate(): void { + const w = Math.max(this.width || 1, 10) + const h = Math.max(this.height || 1, 10) + const ratio = 2 + const canvas = document.createElement('canvas') + canvas.width = Math.ceil(w * ratio) + canvas.height = Math.ceil(h * ratio) + const ctx = canvas.getContext('2d')! + ctx.scale(ratio, ratio) + this.drawGhost(ctx, w, h) + // setElement 会重置 width/height 为位图像素尺寸,缩放须在 setElement 之后按新尺寸计算 + this.setElement(canvas) + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + this.setCoords() + this.canvas?.requestRenderAll() + } + + /** overlay 模式:透明底 + 一圈极淡虚线,保证控件"存在感"且不与 overlay 抢像素 */ + private drawGhost(ctx: CanvasRenderingContext2D, w: number, h: number): void { + ctx.clearRect(0, 0, w, h) + ctx.strokeStyle = GHOST_COLOR + ctx.lineWidth = 0.5 + ctx.setLineDash([3, 3]) + ctx.strokeRect(0.5, 0.5, w - 1, h - 1) + ctx.setLineDash([]) + } + + toControl(): ChartControl { + return { + ...readBaseGeometry(this), + id: this.controlId, + type: 'chart', + kind: this.kind, + categories: this.categories, + series: this.series, + options: this.options, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: ChartControl): void { + this.kind = control.kind + this.categories = control.categories ?? [] + this.series = control.series ?? [] + this.options = control.options + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + this.regenerate() + } +} diff --git a/openprint/src/design/canvas/controls/PrintCircle.ts b/openprint/src/design/canvas/controls/PrintCircle.ts new file mode 100644 index 0000000..28f0d7a --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintCircle.ts @@ -0,0 +1,68 @@ +/** + * PrintCircle —— 圆形控件(复用 rect 协议模型,shape:'circle' 时以 Ellipse 渲染) + */ +import { Ellipse } from 'fabric' +import type { RectControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +export class PrintCircle extends Ellipse implements IPrintObject { + controlId: string + controlType = 'rect' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + constructor(control: RectControl) { + super({ + left: mm(control.left), + top: mm(control.top), + rx: mm(control.width) / 2, + ry: mm(control.height) / 2, + angle: control.angle ?? 0, + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + } + + toControl(): RectControl { + return { + ...readBaseGeometry(this), + type: 'rect', + shape: 'circle', + fill: typeof this.fill === 'string' ? this.fill : undefined, + stroke: typeof this.stroke === 'string' ? this.stroke : undefined, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray as number[] | undefined, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: RectControl): void { + this.set({ + rx: mm(control.width) / 2, + ry: mm(control.height) / 2, + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/openprint/src/design/canvas/controls/PrintImage.ts b/openprint/src/design/canvas/controls/PrintImage.ts new file mode 100644 index 0000000..aa5233e --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintImage.ts @@ -0,0 +1,140 @@ +/** + * PrintImage —— 图片控件(《设计方案》§5.8) + * inline/url/asset/binding 四种来源;无图时显示占位框。 + */ +import { FabricImage, Rect, Group, Textbox, LayoutManager, FixedLayout } from 'fabric' +import type { ImageControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +/** + * 图片控件用 Group 承载:底层是 Rect 占位框,加载成功后替换为 FabricImage。 + * Group 方案对 "加载成功/失败/占位" 三态最稳。 + */ +export class PrintImage extends Group implements IPrintObject { + controlId: string + controlType = 'image' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + value?: ImageControl['value'] + fit: NonNullable = 'contain' + cornerRadius?: number + /** 控件原始几何(mm),loadImage 时 Group width/height 失真的兜底 */ + private _controlWidth = 0 + private _controlHeight = 0 + + constructor(control: ImageControl) { + const w = mm(control.width) + const h = mm(control.height) + const placeholder = new Rect({ + width: w, + height: h, + left: -w / 2, + top: -h / 2, + fill: 'rgba(22,119,255,0.06)', + stroke: '#c0c4cc', + strokeDashArray: [4, 3], + strokeWidth: 1, + rx: control.cornerRadius ?? 0, + ry: control.cornerRadius ?? 0, + }) + const label = new Textbox(control.value?.mode === 'binding' ? `{{${control.value.content}}}` : '图片', { + left: -w / 2, + top: -8, + width: w, + fontSize: 11, + fill: '#909399', + textAlign: 'center', + selectable: false, + evented: false, + }) + super([placeholder, label], { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + // Group 默认 origin 是 center,与 Rect 默认 left/top 不同——统一用 left/top + originX: 'left', + originY: 'top', + // FixedLayout:add/remove 子对象时不重算 left/top,避免上传图片后控件跳位 + layoutManager: new LayoutManager(new FixedLayout()), + }) + this.controlId = control.id + this.value = control.value + this.fit = control.fit ?? 'contain' + this.cornerRadius = control.cornerRadius + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this._controlWidth = control.width + this._controlHeight = control.height + // 从模板恢复时若已有图片内容,立即加载 + void this.loadImage() + } + + /** 异步加载真实图片替换占位(inline/url 来源;binding 设计期保持占位) */ + async loadImage(): Promise { + if (!this.value || this.value.mode === 'binding' || !this.value.content) return + // data: URL 不需要 crossOrigin;外部 URL 设 anonymous 避免画布污染 + const isDataUrl = this.value.content.startsWith('data:') + const loadOpts = isDataUrl ? {} : { crossOrigin: 'anonymous' as const } + try { + const img = await FabricImage.fromURL(this.value.content, loadOpts) + // Group 的 width/height 可能在子对象变化后失真,用控件原始几何兜底 + const w = this.width || mm(this._controlWidth) + const h = this.height || mm(this._controlHeight) + img.set({ + left: -w / 2, + top: -h / 2, + originX: 'left', + originY: 'top', + scaleX: w / (img.width || w), + scaleY: h / (img.height || h), + }) + this.remove(...this.getObjects()) + this.add(img) + // Group.add 触发 enterGroup 会 inverse transform 子对象坐标, + // FixedLayout 下不再有 commitLayout 修正,需手动复位 + img.set({ left: -w / 2, top: -h / 2 }) + this.set({ width: w, height: h }) + this.setCoords() + this.canvas?.requestRenderAll() + } catch { + // 加载失败保持占位框,不阻断设计器 + } + } + + toControl(): ImageControl { + return { + ...readBaseGeometry(this), + type: 'image', + value: this.value, + fit: this.fit, + cornerRadius: this.cornerRadius, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: ImageControl): void { + this.value = control.value + this.fit = control.fit ?? 'contain' + this.cornerRadius = control.cornerRadius + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this._controlWidth = control.width + this._controlHeight = control.height + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + void this.loadImage() + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/openprint/src/design/canvas/controls/PrintLine.ts b/openprint/src/design/canvas/controls/PrintLine.ts new file mode 100644 index 0000000..433126d --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintLine.ts @@ -0,0 +1,67 @@ +/** + * PrintLine —— 线条控件 + * Fabric Line 用 x1/y1/x2/y2 四点 + left/top 定位; + * 协议层只存外接框(left/top/width/height),MVP 仅支持水平/垂直线。 + */ +import { Line } from 'fabric' +import type { LineControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +export class PrintLine extends Line implements IPrintObject { + controlId: string + controlType = 'line' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + constructor(control: LineControl) { + // 水平线:width 有效、height≈0;垂直线反之(MVP) + const w = mm(control.width) + const h = mm(control.height) + const horizontal = h <= 1 + super([0, 0, horizontal ? w : 0, horizontal ? 0 : h], { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + } + + toControl(): LineControl { + const geo = readBaseGeometry(this) + return { + ...geo, + type: 'line', + stroke: typeof this.stroke === 'string' ? this.stroke : undefined, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray ?? undefined, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: LineControl): void { + this.set({ + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/openprint/src/design/canvas/controls/PrintObject.ts b/openprint/src/design/canvas/controls/PrintObject.ts new file mode 100644 index 0000000..98d30ae --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintObject.ts @@ -0,0 +1,66 @@ +/** + * PrintObject —— 所有打印控件的公共契约 + * + * 设计约定(《设计方案》§8.1.2): + * - 每个控件是 Fabric 子类,实例上挂 `controlId` 与 `controlType` + * - `toControl()` 把 Fabric 对象序列化回协议模型(px → mm 由这里统一换算) + * - 属性面板通过 `applyControlProps(control)` 把模型写回 Fabric 对象 + * + * 坐标:协议层 mm(相对 Section 左上角)⇄ 画布层 px(MM_TO_PX,zoom=1)。 + */ +import type { FabricObject } from 'fabric' +import type { AnyControl, ControlType } from '@/types/control' +import { MM_TO_PX } from '@/utils/constants' + +/** 所有打印控件实例都实现的接口 */ +export interface IPrintObject { + controlId: string + controlType: ControlType + /** 所属区域:body 或某个 zone 的 controlId */ + zoneId?: string + toControl(): AnyControl + // 参数声明为 AnyControl(方法双变允许各子类收窄为自己的控件类型) + applyControlProps(control: AnyControl): void +} + +export function isPrintObject(obj: unknown): obj is FabricObject & IPrintObject { + return ( + typeof obj === 'object' && + obj !== null && + 'controlId' in obj && + 'controlType' in obj && + typeof (obj as IPrintObject).toControl === 'function' + ) +} + +/* ------------------------------ 几何换算 helpers ------------------------------ */ + +export const mm = (v: number): number => v * MM_TO_PX +export const px = (v: number): number => v / MM_TO_PX + +/** + * 读取对象的公共几何(px → mm),返回 ControlBase 的几何部分。 + * + * 【坐标系警告】这里的 left/top 是 **Fabric 绝对画布坐标**(页面左上角为原点), + * 而协议层 / store 模型用的是「相对页边距内容区(或所属色带)左上角」。 + * 因此 `toControl()` 的结果 **不可直接写回 store**,必须先经 + * `CanvasDesigner.readControl(obj)` 归一化,否则 `updateControl()` 会再叠加 + * 一次 origin,控件每次操作都向右下平移一个页边距。 + */ +export function readBaseGeometry(obj: FabricObject & IPrintObject) { + // 兜底:fabric 对象的几何在异常时序下可能为 NaN/undefined(如 viewport 未就绪时 + // 创建的对象),`?? 0` 对 NaN 无效,必须用 Number.isFinite 收敛,否则序列化回协议的 + // left/top 带 NaN,写回 store 后 NInputNumber 会渲染删除线。 + const r1 = (n: number): number => Math.round(n * 10) / 10 + const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0) + return { + id: obj.controlId, + left: r1(px(num(obj.left))), + top: r1(px(num(obj.top))), + width: r1(px(num(obj.getScaledWidth()))), + height: r1(px(num(obj.getScaledHeight()))), + angle: num(obj.angle), + } +} + +export const round2 = (n: number): number => Math.round(n * 100) / 100 diff --git a/openprint/src/design/canvas/controls/PrintQrcode.ts b/openprint/src/design/canvas/controls/PrintQrcode.ts new file mode 100644 index 0000000..56b70c2 --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintQrcode.ts @@ -0,0 +1,91 @@ +/** + * PrintQrcode —— 二维码控件(§5.7,qrcode 库离屏渲染) + */ +import { FabricImage } from 'fabric' +import QRCode from 'qrcode' +import type { QrcodeControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +export class PrintQrcode extends FabricImage implements IPrintObject { + controlId: string + controlType = 'qrcode' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + binding?: string + textValue?: string + errorLevel: 'L' | 'M' | 'Q' | 'H' = 'M' + + constructor(control: QrcodeControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.binding = control.binding + this.textValue = control.value + this.errorLevel = control.errorLevel ?? 'M' + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ width: mm(control.width), height: mm(control.height) }) + void this.regenerate() + } + + async regenerate(): Promise { + // 目标显示尺寸(逻辑 px);setElement 会重置 width/height,先保存 + const targetW = this.getScaledWidth() || 1 + const targetH = this.getScaledHeight() || 1 + const text = this.textValue ?? (this.binding ? `{{${this.binding}}}` : 'https://open-print.dev') + const canvas = document.createElement('canvas') + try { + await QRCode.toCanvas(canvas, text, { + width: Math.ceil(Math.max(targetW, targetH) * 2), // 2x 渲染保证缩放清晰 + margin: 1, + errorCorrectionLevel: this.errorLevel, + }) + this.setElement(canvas) + this.set({ scaleX: targetW / canvas.width, scaleY: targetH / canvas.height }) + } catch { + // 内容过长等异常:保持空白占位 + } + this.setCoords() + this.canvas?.requestRenderAll() + } + + toControl(): QrcodeControl { + return { + ...readBaseGeometry(this), + type: 'qrcode', + binding: this.binding, + value: this.textValue, + errorLevel: this.errorLevel, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: QrcodeControl): void { + this.binding = control.binding + this.textValue = control.value + this.errorLevel = control.errorLevel ?? 'M' + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + void this.regenerate() + } +} diff --git a/openprint/src/design/canvas/controls/PrintRect.spec.ts b/openprint/src/design/canvas/controls/PrintRect.spec.ts new file mode 100644 index 0000000..2af8fdc --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintRect.spec.ts @@ -0,0 +1,83 @@ +/** + * PrintRect 四角独立圆角路径几何验证。 + * + * 用 node-canvas 直接执行 traceRoundRectPath(与 Fabric _render 的绘制约定一致: + * ctx 已平移到对象中心,坐标范围 -w/2 ~ w/2),抽样四角像素透明度, + * 确认「某角有半径、其余为 0」时只有该角被圆角化、其余保持直角。 + */ +import { describe, expect, it } from 'vitest' +import { createCanvas } from 'canvas' +import { traceRoundRectPath } from './PrintRect' + +interface Corner { + /** 抽样点(相对对象左上角,单位 px),取靠近外缘但不在圆角切线内的位置 */ + sampleX: number + sampleY: number +} + +function renderAndSample( + w: number, + h: number, + tl: number, + tr: number, + br: number, + bl: number, +): Record<'TL' | 'TR' | 'BR' | 'BL', boolean> { + const pad = 40 + const cw = w + pad * 2 + const ch = h + pad * 2 + const canvas = createCanvas(cw, ch) + const ctx = canvas.getContext('2d')! + ctx.translate(cw / 2, ch / 2) // 模拟 Fabric 把原点移到对象中心 + traceRoundRectPath(ctx as unknown as CanvasRenderingContext2D, w, h, tl, tr, br, bl) + ctx.fillStyle = '#000000' + ctx.fill() + + const img = ctx.getImageData(0, 0, cw, ch)!.data + const alphaAt = (x: number, y: number): boolean => { + const idx = (Math.round(y) * cw + Math.round(x)) * 4 + 3 + return (img[idx] ?? 0) > 10 // 不透明 = 属于形状内部 + } + + // 各角取样点:紧贴外缘、但位于「该角若有大圆角就会被切掉」的位置 + const samples: Record<'TL' | 'TR' | 'BR' | 'BL', Corner> = { + TL: { sampleX: pad + 4, sampleY: pad + 4 }, + TR: { sampleX: pad + w - 4, sampleY: pad + 4 }, + BR: { sampleX: pad + w - 4, sampleY: pad + h - 4 }, + BL: { sampleX: pad + 4, sampleY: pad + h - 4 }, + } + const out = {} as Record<'TL' | 'TR' | 'BR' | 'BL', boolean> + for (const k of Object.keys(samples) as Array) { + out[k] = alphaAt(samples[k].sampleX, samples[k].sampleY) + } + return out +} + +describe('PrintRect —— 四角独立圆角路径几何', () => { + const W = 160 + const H = 100 + + it('仅左上角有半径时,只有 TL 被圆角化(取样点为透明)', () => { + const r = renderAndSample(W, H, 40, 0, 0, 0) + expect(r.TL).toBe(false) // 左上被切成圆角 → 角点透明 + expect(r.TR).toBe(true) // 其余直角 → 角点不透明 + expect(r.BR).toBe(true) + expect(r.BL).toBe(true) + }) + + it('四角全 0 时为标准直角矩形(四角均不透明)', () => { + const r = renderAndSample(W, H, 0, 0, 0, 0) + expect(r.TL && r.TR && r.BR && r.BL).toBe(true) + }) + + it('四角均设大半径时为全圆角(四角均透明)', () => { + const r = renderAndSample(W, H, 30, 30, 30, 30) + expect(r.TL && r.TR && r.BR && r.BL).toBe(false) + }) + + it('半径上限被夹紧到短边一半,不溢出(超大半径下四角仍正常)', () => { + const r = renderAndSample(W, H, 999, 999, 999, 999) + // 短边=H=100 → 上限 50;四角应被夹紧为圆角但形状完整 + expect(r.TL && r.TR && r.BR && r.BL).toBe(false) + }) +}) diff --git a/openprint/src/design/canvas/controls/PrintRect.ts b/openprint/src/design/canvas/controls/PrintRect.ts new file mode 100644 index 0000000..321ce30 --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintRect.ts @@ -0,0 +1,152 @@ +/** + * PrintRect —— 矩形控件 + * + * 圆角走「四角独立半径」绘制:Fabric 原生 Rect 仅支持统一 rx/ry, + * 这里重写 `_render`,用 arcTo 画出四角各自独立的圆角矩形路径, + * 单位 px(与协议 cornerRadius* 一致)。 + */ +import { Rect } from 'fabric' +import type { RectControl } from '@/types/control' +import { mm, px, readBaseGeometry, type IPrintObject } from './PrintObject' + +/** + * 以对象中心为原点绘制四角独立圆角的矩形路径。 + * ctx 进入时已被 Fabric 平移到对象中心(坐标范围 -w/2 ~ w/2), + * 半径单位为对象本地像素(drawing space,未乘 scaleX/scaleY)。 + */ +export function traceRoundRectPath( + ctx: CanvasRenderingContext2D, + w: number, + h: number, + tl: number, + tr: number, + br: number, + bl: number, +): void { + const x = -w / 2 + const y = -h / 2 + // 单角半径上限取短边一半,避免相邻圆角重叠产生扭曲 + const maxR = Math.min(w, h) / 2 + const clamp = (r: number): number => Math.max(0, Math.min(Math.round(r), maxR)) + const a = clamp(tl) + const b = clamp(tr) + const c = clamp(br) + const d = clamp(bl) + ctx.beginPath() + ctx.moveTo(x + a, y) + ctx.lineTo(x + w - b, y) + if (b > 0) ctx.arcTo(x + w, y, x + w, y + b, b) + ctx.lineTo(x + w, y + h - c) + if (c > 0) ctx.arcTo(x + w, y + h, x + w - c, y + h, c) + ctx.lineTo(x + d, y + h) + if (d > 0) ctx.arcTo(x, y + h, x, y + h - d, d) + ctx.lineTo(x, y + a) + if (a > 0) ctx.arcTo(x, y, x + a, y, a) + ctx.closePath() +} + +export class PrintRect extends Rect implements IPrintObject { + controlId: string + controlType = 'rect' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + /** 四角独立圆角(px),绘制时优先于 rx/ry */ + cornerRadiusTL = 0 + cornerRadiusTR = 0 + cornerRadiusBR = 0 + cornerRadiusBL = 0 + /** 统一圆角(px),四角独立值统一回落基准 */ + cornerRadius = 0 + + constructor(control: RectControl) { + const cr = control.cornerRadius ?? 0 + super({ + left: mm(control.left), + top: mm(control.top), + width: mm(control.width), + height: mm(control.height), + angle: control.angle ?? 0, + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + rx: cr, + ry: cr, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.cornerRadiusTL = control.cornerRadiusTL ?? cr + this.cornerRadiusTR = control.cornerRadiusTR ?? cr + this.cornerRadiusBR = control.cornerRadiusBR ?? cr + this.cornerRadiusBL = control.cornerRadiusBL ?? cr + this.cornerRadius = cr + } + + /** 四角独立圆角路径绘制(覆盖 Fabric 原生统一 rx/ry) */ + _render(ctx: CanvasRenderingContext2D): void { + traceRoundRectPath( + ctx, + this.width, + this.height, + this.cornerRadiusTL, + this.cornerRadiusTR, + this.cornerRadiusBR, + this.cornerRadiusBL, + ) + this._renderFill(ctx) + this._renderStroke(ctx) + } + + toControl(): RectControl { + const cr = this.cornerRadius ?? 0 + const out: RectControl = { + ...readBaseGeometry(this), + type: 'rect', + fill: typeof this.fill === 'string' ? this.fill : undefined, + stroke: typeof this.stroke === 'string' ? this.stroke : undefined, + strokeWidth: this.strokeWidth, + cornerRadius: cr || undefined, + strokeDashArray: this.strokeDashArray as number[] | undefined, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + // 仅当某角与统一值不一致时才落盘为独立覆盖值,保持 JSON 干净 + if (this.cornerRadiusTL !== cr) out.cornerRadiusTL = this.cornerRadiusTL || undefined + if (this.cornerRadiusTR !== cr) out.cornerRadiusTR = this.cornerRadiusTR || undefined + if (this.cornerRadiusBR !== cr) out.cornerRadiusBR = this.cornerRadiusBR || undefined + if (this.cornerRadiusBL !== cr) out.cornerRadiusBL = this.cornerRadiusBL || undefined + return out + } + + applyControlProps(control: RectControl): void { + const cr = control.cornerRadius ?? 0 + this.cornerRadiusTL = control.cornerRadiusTL ?? cr + this.cornerRadiusTR = control.cornerRadiusTR ?? cr + this.cornerRadiusBR = control.cornerRadiusBR ?? cr + this.cornerRadiusBL = control.cornerRadiusBL ?? cr + this.cornerRadius = cr + this.set({ + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + rx: cr, + ry: cr, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/openprint/src/design/canvas/controls/PrintRichText.ts b/openprint/src/design/canvas/controls/PrintRichText.ts new file mode 100644 index 0000000..aef31ce --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintRichText.ts @@ -0,0 +1,149 @@ +/** + * PrintRichText —— 富文本控件(§5.8) + * + * 设计期显示:把富文本 HTML 消毒后包进 SVG ``, + * 再栅格化成位图给 FabricImage 显示(与导出引擎同一技术路线,Chromium 限定)。 + * 渲染期(预览/导出/打印)走 data-binder → html-renderer 的 `kind:'html'` 链路, + * 本类只负责画布上的"所见即所得"预览。 + */ +import { FabricImage } from 'fabric' +import DOMPurify from 'dompurify' +import type { RichTextControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' +import { toXmlSafe, svgToCanvas } from '@/core/export-engine/rasterize' + +/** 设计期默认字号(px,foreignObject 内继承) */ +const DESIGN_FONT_PX = 14 + +export class PrintRichText extends FabricImage implements IPrintObject { + controlId: string + controlType = 'richtext' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + /** 富文本 HTML(协议字段 value) */ + htmlValue?: string + + /** 渲染序号:连续输入时只应用最新一次栅格化结果,防止旧结果覆盖新内容 */ + private renderSeq = 0 + + constructor(control: RichTextControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.htmlValue = control.value + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + // 初始尺寸(未缩放),regenerate 会按它换算 scale + this.set({ width: mm(control.width), height: mm(control.height) }) + void this.regenerate() + } + + /** 重新栅格化富文本位图,并按控件宽高缩放显示 */ + async regenerate(): Promise { + const w = this.width || 1 + const h = this.height || 1 + const seq = ++this.renderSeq + const canvas = await renderRichTextToCanvas(this.htmlValue ?? '', w, h) + if (seq !== this.renderSeq) return // 已有更新的渲染,丢弃过期结果 + if (canvas) { + this.setElement(canvas) + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + } + this.setCoords() + this.canvas?.requestRenderAll() + } + + toControl(): RichTextControl { + return { + ...readBaseGeometry(this), + type: 'richtext', + value: this.htmlValue, + printable: this.printable, + visibleIf: this.visibleIf, + locked: this.lockMovementX && this.lockMovementY ? true : undefined, + name: this.controlName, + } + } + + applyControlProps(control: RichTextControl): void { + this.htmlValue = control.value + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + void this.regenerate() + } +} + +/* ------------------------------ 位图渲染 helpers ------------------------------ */ + +/** 富文本 HTML → 设计期位图 canvas(失败降级为纯文本占位) */ +async function renderRichTextToCanvas( + html: string, + w: number, + h: number, +): Promise { + try { + // 画布预览同样必须消毒(防模板注入),渲染期有 data-binder 二次消毒 + const safe = toXmlSafe(DOMPurify.sanitize(html || '', { USE_PROFILES: { html: true } })) + const svg = + `` + + `` + + `
` + + `${safe}
` + return await svgToCanvas(svg, 1, '#ffffff') + } catch { + return drawFallback(stripHtml(html), w, h) + } +} + +/** 降级占位:白底 + 纯文本(SVG 光栅化失败时保证画布上仍能看到内容) */ +function drawFallback(text: string, w: number, h: number): HTMLCanvasElement { + const canvas = document.createElement('canvas') + canvas.width = Math.max(1, Math.ceil(w)) + canvas.height = Math.max(1, Math.ceil(h)) + const ctx = canvas.getContext('2d') + if (!ctx) return canvas + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.fillStyle = '#000000' + ctx.font = `${DESIGN_FONT_PX}px system-ui, sans-serif` + const lines = (text || '富文本').split('\n') + const maxLines = Math.max(1, Math.floor(canvas.height / (DESIGN_FONT_PX + 4))) + const step = DESIGN_FONT_PX + 4 + for (let i = 0; i < Math.min(lines.length, maxLines); i++) { + ctx.fillText((lines[i] ?? '').slice(0, 80), 4, 4 + i * step) + } + return canvas +} + +/** 剥标签 + 解码实体的纯文本提取(仅用于降级显示) */ +function stripHtml(html: string): string { + return String(html ?? '') + .replace(/<[^>]*>/g, '') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/'/gi, "'") + .replace(/\s+/g, ' ') + .trim() +} diff --git a/openprint/src/design/canvas/controls/PrintTable.ts b/openprint/src/design/canvas/controls/PrintTable.ts new file mode 100644 index 0000000..af65257 --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintTable.ts @@ -0,0 +1,231 @@ +/** + * PrintTable —— 表格控件(《设计方案》§5.4) + * + * 【方案 A】表格的**视觉**由 HTML overlay(TableViewLayer.vue + table-design-render.ts)负责, + * 与运行期 html-renderer 共用同一套类名与样式语义,做到设计即打印、零漂移。 + * 本 Fabric 对象退化为「透明宿主」:只承担选中 / 拖拽 / 缩放手柄 / 层级 / 序列化, + * 位图上仅画一圈极淡的虚线轮廓,作为 overlay 未就绪时的兜底提示。 + * + * 若把 `htmlPreview` 置为 false(单测 / 降级),则回落到旧的 Fabric 2D 预览。 + */ +import { FabricImage } from 'fabric' +import type { TableCell, TableControl, TableColumn } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' +import { ptToPx } from '@/core/units' + +const BORDER_COLOR = '#333333' +const HEADER_BG = '#F5F7FA' +const TEXT_COLOR = '#1f2329' +const PLACEHOLDER_COLOR = '#606266' +/** overlay 兜底轮廓色(极淡,正常情况下被 overlay 的真实边框完全覆盖) */ +const GHOST_COLOR = 'rgba(120,130,145,0.35)' + +export class PrintTable extends FabricImage implements IPrintObject { + controlId: string + controlType = 'table' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + dataSource?: string + /** 内嵌数据行(导入数据场景;与 dataSource 解耦,运行期直接作为表格数据) */ + data?: Array> + columns: TableColumn[] + options: TableControl['options'] + + /** 设计期单元格网格(方案 A:双击可编辑的内容与样式载体) */ + cells?: TableCell[][] + headerRows?: number + staticRows?: number + designRows?: number + + /** true = 视觉交给 HTML overlay,位图只画兜底轮廓 */ + htmlPreview = true + + constructor(control: TableControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.dataSource = control.dataSource + this.data = control.data + this.columns = control.columns + this.options = control.options + this.cells = control.cells + this.headerRows = control.headerRows + this.staticRows = control.staticRows + this.designRows = control.designRows + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ width: mm(control.width), height: mm(control.height) }) + this.regenerate() + } + + /** 重绘设计期预览(2x 分辨率保证缩放清晰) */ + regenerate(): void { + const w = Math.max(this.width || 1, 10) + const h = Math.max(this.height || 1, 10) + const ratio = 2 + const canvas = document.createElement('canvas') + canvas.width = Math.ceil(w * ratio) + canvas.height = Math.ceil(h * ratio) + const ctx = canvas.getContext('2d')! + ctx.scale(ratio, ratio) + + if (this.htmlPreview) { + this.drawGhost(ctx, w, h) + } else { + this.drawLegacyPreview(ctx, w, h, ratio) + } + + // 注意:setElement 会把 width/height 重置为位图像素尺寸,缩放必须在 setElement 之后按新尺寸计算 + this.setElement(canvas) + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + this.setCoords() + this.canvas?.requestRenderAll() + } + + /** overlay 模式:透明底 + 一圈极淡虚线,保证控件"存在感"且不与 overlay 抢像素 */ + private drawGhost(ctx: CanvasRenderingContext2D, w: number, h: number): void { + ctx.clearRect(0, 0, w, h) + ctx.strokeStyle = GHOST_COLOR + ctx.lineWidth = 0.5 + ctx.setLineDash([3, 3]) + ctx.strokeRect(0.5, 0.5, w - 1, h - 1) + ctx.setLineDash([]) + } + + /** 旧版 Fabric 2D 预览(降级路径,行为与方案 A 之前一致) */ + private drawLegacyPreview(ctx: CanvasRenderingContext2D, w: number, h: number, ratio: number): void { + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, w, h) + + const headerH = Math.min(28, h * 0.3) + const dataRowH = Math.min(24, (h - headerH) / 4) + const fontSize = ptToPx(9) + + // 列宽按比例分配(协议列宽是 mm,控件宽是 px,归一化即可) + const totalColMm = this.columns.reduce((s, c) => s + c.width, 0) || 1 + const colXs: number[] = [0] + for (const col of this.columns) { + colXs.push(colXs[colXs.length - 1]! + (col.width / totalColMm) * w) + } + + // 表头 + ctx.fillStyle = HEADER_BG + ctx.fillRect(0, 0, w, headerH) + ctx.font = `bold ${fontSize}px "Source Han Sans CN", "PingFang SC", sans-serif` + ctx.fillStyle = TEXT_COLOR + ctx.textBaseline = 'middle' + this.columns.forEach((col, i) => { + const x0 = colXs[i]! + const cw = colXs[i + 1]! - x0 + ctx.save() + ctx.beginPath() + ctx.rect(x0, 0, cw, headerH) + ctx.clip() + const tx = col.headerAlign === 'center' || (!col.headerAlign && col.align === 'center') + ? x0 + cw / 2 + : col.headerAlign === 'right' || (!col.headerAlign && col.align === 'right') + ? x0 + cw - 4 + : x0 + 4 + ctx.textAlign = col.headerAlign ?? col.align ?? 'left' + ctx.fillText(col.title || ' ', tx, headerH / 2) + ctx.restore() + }) + + // 示例数据行(绑定字段占位符) + ctx.font = `${fontSize}px "Source Han Sans CN", "PingFang SC", sans-serif` + this.columns.forEach((col, i) => { + const x0 = colXs[i]! + const cw = colXs[i + 1]! - x0 + const cellText = col.expression ?? (col.field ? `{{item.${col.field}}}` : '') + if (!cellText) return + ctx.save() + ctx.beginPath() + ctx.rect(x0, headerH, cw, dataRowH) + ctx.clip() + ctx.fillStyle = PLACEHOLDER_COLOR + ctx.textAlign = col.align ?? 'left' + const tx = col.align === 'center' ? x0 + cw / 2 : col.align === 'right' ? x0 + cw - 4 : x0 + 4 + ctx.fillText(cellText, tx, headerH + dataRowH / 2) + ctx.restore() + }) + + // 边框线 + const borders = this.options?.borders ?? 'all' + ctx.strokeStyle = BORDER_COLOR + ctx.lineWidth = 1 / ratio > 0.5 ? 1 : 0.75 + if (borders !== 'none') { + ctx.strokeRect(0.5, 0.5, w - 1, h - 1) + ctx.beginPath() + ctx.moveTo(0, headerH) + ctx.lineTo(w, headerH) + ctx.stroke() + if (borders === 'all') { + for (let i = 1; i < colXs.length - 1; i++) { + ctx.beginPath() + ctx.moveTo(colXs[i]!, 0) + ctx.lineTo(colXs[i]!, h) + ctx.stroke() + } + for (let r = 1; r <= 2; r++) { + const y = headerH + dataRowH * r + if (y < h) { + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(w, y) + ctx.stroke() + } + } + } + } + } + + toControl(): TableControl { + return { + ...readBaseGeometry(this), + type: 'table', + dataSource: this.dataSource, + data: this.data, + columns: this.columns, + options: this.options, + cells: this.cells, + headerRows: this.headerRows, + staticRows: this.staticRows, + designRows: this.designRows, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: TableControl): void { + this.dataSource = control.dataSource + this.data = control.data + this.columns = control.columns + this.options = control.options + this.cells = control.cells + this.headerRows = control.headerRows + this.staticRows = control.staticRows + this.designRows = control.designRows + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + this.regenerate() + } +} diff --git a/openprint/src/design/canvas/controls/PrintText.ts b/openprint/src/design/canvas/controls/PrintText.ts new file mode 100644 index 0000000..d83e72e --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintText.ts @@ -0,0 +1,111 @@ +/** + * PrintText —— 文本控件(《设计方案》§5.2 / §5.3) + * 基于 fabric.Textbox:对齐/字体/颜色均为原生属性。 + * 字号协议层是 pt,Fabric 用 px,此处统一换算。 + */ +import { Textbox } from 'fabric' +import type { TextControl, TextStyle } from '@/types/control' +import { ptToPx, pxToPt } from '@/core/units' +import { mm, px, readBaseGeometry, round2, type IPrintObject } from './PrintObject' + +export class PrintText extends Textbox implements IPrintObject { + controlId: string + controlType = 'text' as const + zoneId?: string + + /** 协议 extras(设计期保留,显示文本只是占位) */ + binding?: string + expression?: string + controlFormat?: TextControl['format'] + printable = true + visibleIf?: string + controlName?: string + + constructor(control: TextControl) { + super(displayText(control), { + left: mm(control.left), + top: mm(control.top), + width: mm(control.width), + angle: control.angle ?? 0, + ...styleToFabric(control.style), + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.zoneId = undefined + this.binding = control.binding + this.expression = control.expression + this.controlFormat = control.format + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + } + + /** 序列化回协议模型(px → mm / px → pt) */ + toControl(): TextControl { + return { + ...readBaseGeometry(this), + type: 'text', + value: this.binding || this.expression ? undefined : this.text, + binding: this.binding, + expression: this.expression, + format: this.controlFormat, + printable: this.printable, + visibleIf: this.visibleIf, + locked: this.lockMovementX && this.lockMovementY ? true : undefined, + name: this.controlName, + style: { + fontSize: round2(pxToPt(this.fontSize)), + fill: typeof this.fill === 'string' ? this.fill : undefined, + fontFamily: this.fontFamily, + fontWeight: (this.fontWeight as 'normal' | 'bold') || undefined, + fontStyle: this.fontStyle === 'italic' ? 'italic' : undefined, + textDecoration: this.underline ? 'underline' : undefined, + textAlign: (this.textAlign as TextStyle['textAlign']) || undefined, + lineHeight: this.lineHeight, + letterSpacing: this.charSpacing ? round2(pxToPt(this.charSpacing / 1000 * this.fontSize)) : undefined, + }, + } + } + + /** 属性面板编辑后回写(不触发 object:modified) */ + applyControlProps(control: TextControl): void { + this.set({ + ...styleToFabric(control.style), + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.binding = control.binding + this.expression = control.expression + this.controlFormat = control.format + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.text = displayText(control) + // 非绑定文本允许直接改内容尺寸 + if (control.width) this.set('width', mm(control.width)) + this.setCoords() + this.canvas?.requestRenderAll() + } +} + +/** 设计期显示文本:绑定/表达式显示占位符,静态文本显示本体 */ +function displayText(control: TextControl): string { + if (control.expression) return control.expression + if (control.binding) return `{{${control.binding}}}` + return control.value ?? '文本' +} + +function styleToFabric(style?: TextStyle) { + return { + fontSize: ptToPx(style?.fontSize ?? 12), + fill: style?.fill ?? '#000000', + fontFamily: style?.fontFamily ?? 'Source Han Sans CN, PingFang SC, sans-serif', + fontWeight: style?.fontWeight ?? 'normal', + fontStyle: style?.fontStyle ?? 'normal', + underline: style?.textDecoration === 'underline', + textAlign: style?.textAlign ?? 'left', + lineHeight: style?.lineHeight ?? 1.16, + charSpacing: style?.letterSpacing ? (style.letterSpacing / (style.fontSize ?? 12)) * 1000 : 0, + } +} diff --git a/openprint/src/design/canvas/controls/PrintZone.ts b/openprint/src/design/canvas/controls/PrintZone.ts new file mode 100644 index 0000000..cd202e9 --- /dev/null +++ b/openprint/src/design/canvas/controls/PrintZone.ts @@ -0,0 +1,107 @@ +/** + * PrintZone —— 页眉/页脚区域控件(《设计方案》§5.14) + * + * 画布表现:一条横向锁定色带(页顶或页底), + * - 自身不可移动/旋转(位置由页顶/页底固定) + * - 仅保留内边缘一个 resize 手柄改高度(header=下边缘 mb,footer=上边缘 mt) + * - 单例:每页最多一个 header + 一个 footer(由 CanvasDesigner.addZone 保证) + * - 序列化目标是 sections[header/footer],不进 body.components + */ +import { Rect } from 'fabric' +import type { ZoneControl } from '@/types/control' +import { mm, px, round2, type IPrintObject } from './PrintObject' + +export class PrintZone extends Rect implements IPrintObject { + controlId: string + controlType = 'zone' as const + + zone: 'header' | 'footer' + repeat = true + + constructor(control: ZoneControl, pageWidthMm: number, pageHeightMm: number) { + const h = mm(control.zoneHeight) + super({ + left: 0, + top: control.zone === 'header' ? 0 : mm(pageHeightMm) - h, + width: mm(pageWidthMm), + height: h, + fill: control.zone === 'header' ? 'rgba(22,119,255,0.05)' : 'rgba(22,119,255,0.04)', + stroke: 'rgba(22,119,255,0.45)', + strokeWidth: 1, + strokeDashArray: [6, 3], + // 位置锁定:不可移动/旋转/横拉 + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + hasControls: true, + hasBorders: true, + selectable: true, + }) + this.controlId = control.id + this.zone = control.zone + this.repeat = control.repeat ?? true + this.restrictControls() + } + + /** 只留内边缘手柄:header 用底部中点(mb),footer 用顶部中点(mt) */ + private restrictControls(): void { + const edge = this.zone === 'header' ? 'mb' : 'mt' + const controls = { ...this.controls } + for (const key of Object.keys(controls)) { + if (key !== edge) { + // 其余手柄禁用 + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete controls[key] + } + } + this.controls = controls + } + + /** 页面尺寸变化时由 CanvasDesigner 调用,重贴页顶/页底 */ + relayout(pageWidthMm: number, pageHeightMm: number): void { + const h = this.height * this.scaleY + this.set({ + left: 0, + width: mm(pageWidthMm), + scaleX: 1, + scaleY: 1, + height: h, + top: this.zone === 'header' ? 0 : mm(pageHeightMm) - h, + }) + this.setCoords() + } + + get zoneHeightMm(): number { + return round2(px(this.height * this.scaleY)) + } + + toControl(): ZoneControl { + return { + id: this.controlId, + type: 'zone', + zone: this.zone, + left: 0, + top: 0, + width: round2(px(this.getScaledWidth())), + height: this.zoneHeightMm, + zoneHeight: this.zoneHeightMm, + repeat: this.repeat, + printable: true, + children: [], // 子组件由 CanvasDesigner.serialize 按几何归属填充 + } + } + + applyControlProps(control: ZoneControl): void { + this.repeat = control.repeat ?? true + if (control.zoneHeight && control.zoneHeight !== this.zoneHeightMm) { + const pageH = this.canvas ? px(this.canvas.getHeight()) : 0 + this.set({ + scaleY: 1, + height: mm(control.zoneHeight), + top: this.zone === 'header' ? 0 : mm(pageH) - mm(control.zoneHeight), + }) + this.setCoords() + } + this.canvas?.requestRenderAll() + } +} diff --git a/openprint/src/design/canvas/controls/index.ts b/openprint/src/design/canvas/controls/index.ts new file mode 100644 index 0000000..9ed1661 --- /dev/null +++ b/openprint/src/design/canvas/controls/index.ts @@ -0,0 +1,83 @@ +/** + * 控件注册表:协议模型 → Fabric 对象 的统一工厂 + */ +import type { FabricObject } from 'fabric' +import type { AnyControl } from '@/types/control' +import { PrintText } from './PrintText' +import { PrintRect } from './PrintRect' +import { PrintCircle } from './PrintCircle' +import { PrintLine } from './PrintLine' +import { PrintImage } from './PrintImage' +import { PrintBarcode } from './PrintBarcode' +import { PrintQrcode } from './PrintQrcode' +import { PrintTable } from './PrintTable' +import { PrintZone } from './PrintZone' +import { PrintRichText } from './PrintRichText' +import { PrintChart } from './PrintChart' +import type { IPrintObject } from './PrintObject' + +export { + PrintText, + PrintRect, + PrintCircle, + PrintLine, + PrintImage, + PrintBarcode, + PrintQrcode, + PrintTable, + PrintZone, + PrintRichText, + PrintChart, +} +export { isPrintObject, type IPrintObject } from './PrintObject' + +export interface PageSizeMm { + widthMm: number + heightMm: number +} + +/** 由协议模型创建 Fabric 控件对象 */ +export function createFabricControl( + control: AnyControl, + page: PageSizeMm, +): FabricObject & IPrintObject { + switch (control.type) { + case 'text': + return new PrintText(control) + case 'rect': + return control.shape === 'circle' ? new PrintCircle(control) : new PrintRect(control) + case 'line': + return new PrintLine(control) + case 'image': { + const obj = new PrintImage(control) + void obj.loadImage() + return obj + } + case 'barcode': + return new PrintBarcode(control) + case 'qrcode': + return new PrintQrcode(control) + case 'table': + return new PrintTable(control) + case 'chart': + return new PrintChart(control) + case 'zone': + return new PrintZone(control, page.widthMm, page.heightMm) + case 'richtext': + return new PrintRichText(control) + } +} + +/** 控件类型的中文名(图层/面板显示用) */ +export const CONTROL_TYPE_LABEL: Record = { + text: '文本', + image: '图片', + table: '表格', + barcode: '条码', + qrcode: '二维码', + rect: '矩形', + line: '线条', + richtext: '富文本', + zone: '区域', + chart: '图表', +} diff --git a/openprint/src/design/canvas/guides/SmartGuides.ts b/openprint/src/design/canvas/guides/SmartGuides.ts new file mode 100644 index 0000000..a50bd8f --- /dev/null +++ b/openprint/src/design/canvas/guides/SmartGuides.ts @@ -0,0 +1,583 @@ +/** + * SmartGuides —— 智能吸附对齐线(增强版) + * + * 拖拽控件时与其他控件/页面/页边距的边缘、中心对齐吸附, + * 四边同时检测,命中后在 contextTop 上画参考线。 + * Shift 按住时临时禁用吸附。 + * + * 渲染内容: + * 1. 蓝色虚线边界辅助线 —— 沿元素上下左右 4 条边横穿整个画布,对齐标尺 + * 2. 红色实线 —— 边缘/中心吸附命中线(Figma 红粉) + * 3. 绿色标签 —— 吸附间距(mm 数值) + * 4. 灰色标签 —— 元素实时坐标(mm) + */ +import type { Canvas, FabricObject, Rect } from 'fabric' +import { MM_TO_PX, DEFAULT_SNAP_THRESHOLD, RULER_THICK } from '@/utils/constants' +import { isPrintObject } from '../controls' +import { rulerBand } from '../rulers/rulerHighlight' + +const SNAP_PX = DEFAULT_SNAP_THRESHOLD * MM_TO_PX + +/** 配色方案: + * 蓝 = 元素边界辅助线(用户指定) + * 玫红 = 吸附命中(与边界线区分) + * 翠绿 = 距离标签 + * 灰 = 实时坐标信息 + * 护眼绿底白字药丸 = 辅助线端点坐标读数 + */ +const COLOR_BOUNDING = '#378ADD' +const COLOR_SNAP = '#F43F5E' +const COLOR_DISTANCE = '#10B981' +const COLOR_POSITION = '#AFB4BD' +/** 药丸底色(护眼绿,白字;不透明保证浅/深色画布均清晰) */ +const CHIP_BG = '#3FA776' +const CHIP_BORDER = 'rgba(255,255,255,0.45)' +const CHIP_FG = '#FFFFFF' + +/* ------------------------------------------------------------------ */ +/* 类型定义 */ +/* ------------------------------------------------------------------ */ + +interface SnapCandidate { + value: number + orientation: 'v' | 'h' + source: 'page' | 'margin' | 'element' +} + +interface SnapResult { + line: number + orientation: 'v' | 'h' + source: 'page' | 'margin' | 'element' + /** 命中的吸附候选值 */ + candidateValue: number + /** 目标边的原始值 */ + targetValue: number +} + +interface MarginMm { + top: number + bottom: number + left: number + right: number +} + +export interface SmartGuidesConfig { + /** 网格尺寸(mm) */ + gridSizeMm: number + /** 视觉网格线开关(仅作参考、不强绑定元素) */ + gridVisible: boolean + /** 视觉网格线颜色(rgba / hex) */ + gridColor: string +} + +/* ------------------------------------------------------------------ */ +/* SmartGuides */ +/* ------------------------------------------------------------------ */ + +export class SmartGuides { + private canvas: Canvas + private getPage: () => Rect + private getMarginMm: () => MarginMm + private snapResults: SnapResult[] = [] + private movingTarget: FabricObject | null = null + private shiftHeld = false + private lastSnapV: SnapResult | null = null + private lastSnapH: SnapResult | null = null + + /** 网格配置(由 CanvasDesigner 通过 setter 控制;仅视觉参考,不吸附) */ + config: SmartGuidesConfig = { + gridSizeMm: 5, + gridVisible: false, + gridColor: 'rgba(120,130,145,0.5)', + } + + /* ---------- 事件处理 ---------- */ + + private onMoving = (e: { target?: FabricObject }) => { + const target = e.target + if (!target) { + this.movingTarget = null + this.snapResults = [] + rulerBand.value = null + return + } + this.movingTarget = target + if (!this.shiftHeld) this.snap(target) + this.syncBand(target) + } + + /** 缩放时也同步标尺高亮带(不改辅助线,只反映尺寸变化) */ + private onScaling = (e: { target?: FabricObject }) => { + const target = e.target + if (target) this.syncBand(target) + } + + /** 选中/切换选中:把高亮带对准当前活动元素 */ + private onSelection = () => { + const obj = this.canvas.getActiveObject() + if (obj && isPrintObject(obj)) this.syncBand(obj) + else rulerBand.value = null + } + + private onSelectionCleared = () => { + rulerBand.value = null + } + + /** 把元素的包围盒(画布逻辑 px)写入标尺高亮带;非法尺寸则清除 */ + private syncBand(target: FabricObject): void { + const b = target.getBoundingRect() + if (b.width > 0 && b.height > 0) { + rulerBand.value = { left: b.left, top: b.top, width: b.width, height: b.height } + } else { + rulerBand.value = null + } + } + + private onUp = () => { + this.movingTarget = null + this.snapResults = [] + // 注意:不清空 rulerBand —— 拖拽结束后元素仍处于选中态,高亮带保留反映其位置/尺寸 + this.canvas.requestRenderAll() + } + + private onAfterRender = (opt: { ctx: CanvasRenderingContext2D }) => { + // 网格始终可见(独立于拖拽) + this.drawGrid(opt.ctx) + // 拖拽时覆盖吸附辅助线(网格上叠加) + if (this.movingTarget) { + this.draw(opt.ctx) + } + // 常驻辅助线:开启 showGuides 的控件始终显示(与拖拽状态无关) + this.drawPersistent(opt.ctx) + } + + private onKey = (e: KeyboardEvent) => { + if (e.key === 'Shift') this.shiftHeld = e.type === 'keydown' + } + + /* ---------- 生命周期 ---------- */ + + constructor( + canvas: Canvas, + getPage: () => Rect, + getMarginMm: () => MarginMm, + ) { + this.canvas = canvas + this.getPage = getPage + this.getMarginMm = getMarginMm + + canvas.on('object:moving', this.onMoving) + canvas.on('object:scaling', this.onScaling) + canvas.on('mouse:up', this.onUp) + canvas.on('selection:created', this.onSelection) + canvas.on('selection:updated', this.onSelection) + canvas.on('selection:cleared', this.onSelectionCleared) + canvas.on('after:render', this.onAfterRender) + window.addEventListener('keydown', this.onKey) + window.addEventListener('keyup', this.onKey) + } + + dispose(): void { + this.canvas.off('object:moving', this.onMoving) + this.canvas.off('object:scaling', this.onScaling) + this.canvas.off('mouse:up', this.onUp) + this.canvas.off('selection:created', this.onSelection) + this.canvas.off('selection:updated', this.onSelection) + this.canvas.off('selection:cleared', this.onSelectionCleared) + this.canvas.off('after:render', this.onAfterRender) + window.removeEventListener('keydown', this.onKey) + window.removeEventListener('keyup', this.onKey) + } + + /* ---------- 候选集收集 ---------- */ + + private collectCandidates(skip: FabricObject): SnapCandidate[] { + const page = this.getPage() + const margin = this.getMarginMm() + const pw = page.width + const ph = page.height + const ml = margin.left * MM_TO_PX + const mr = (210 - margin.right) * MM_TO_PX + const mt = margin.top * MM_TO_PX + const mb = (297 - margin.bottom) * MM_TO_PX + const results: SnapCandidate[] = [] + + // 1. 页面边缘 + 中心(始终存在于未命名候选列表中) + const addV = (value: number, source: SnapCandidate['source']) => results.push({ value, orientation: 'v', source }) + const addH = (value: number, source: SnapCandidate['source']) => results.push({ value, orientation: 'h', source }) + + // 页面 + addV(0, 'page'); addV(pw / 2, 'page'); addV(pw, 'page') + addH(0, 'page'); addH(ph / 2, 'page'); addH(ph, 'page') + + // 页边距(内边界) + addV(ml, 'margin'); addV(mr, 'margin') + addH(mt, 'margin'); addH(mb, 'margin') + // 页边距中心 + addV(ml + (mr - ml) / 2, 'margin') + addH(mt + (mb - mt) / 2, 'margin') + + // 2. 其他控件 + for (const obj of this.canvas.getObjects()) { + if (obj === skip || obj === page || !isPrintObject(obj)) continue + const b = obj.getBoundingRect() + if (b.width === 0 && b.height === 0) continue + addV(b.left, 'element') + addV(b.left + b.width / 2, 'element') + addV(b.left + b.width, 'element') + addH(b.top, 'element') + addH(b.top + b.height / 2, 'element') + addH(b.top + b.height, 'element') + } + + return results + } + + /* ---------- 吸附计算 ---------- */ + + private snap(target: FabricObject): void { + const b = target.getBoundingRect() + if (b.width === 0 && b.height === 0) { this.snapResults = []; return } + + const candidates = this.collectCandidates(target) + const vCands = candidates.filter((c) => c.orientation === 'v') + const hCands = candidates.filter((c) => c.orientation === 'h') + + // 目标六条参考位置 + const vTargets = [ + { key: 'left', value: b.left }, + { key: 'centerX', value: b.left + b.width / 2 }, + { key: 'right', value: b.left + b.width }, + ] + const hTargets = [ + { key: 'top', value: b.top }, + { key: 'centerY', value: b.top + b.height / 2 }, + { key: 'bottom', value: b.top + b.height }, + ] + + this.snapResults = [] + + // --- 记录所有命中(用于渲染辅助线) --- + for (const t of vTargets) { + for (const c of vCands) { + if (Math.abs(c.value - t.value) <= SNAP_PX) { + this.snapResults.push({ + line: c.value, + orientation: 'v', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + }) + } + } + } + for (const t of hTargets) { + for (const c of hCands) { + if (Math.abs(c.value - t.value) <= SNAP_PX) { + this.snapResults.push({ + line: c.value, + orientation: 'h', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + }) + } + } + } + + // --- 取最佳吸附线应用位置修正(候选线吸附;网格仅作参考、不强绑定) --- + let bestV: SnapResult | null = null + let bestVDist = Infinity + for (const t of vTargets) { + for (const c of vCands) { + const dist = Math.abs(c.value - t.value) + if (dist <= SNAP_PX && dist < bestVDist) { + bestVDist = dist + bestV = { + line: c.value, + orientation: 'v', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + } + } + } + } + let bestH: SnapResult | null = null + let bestHDist = Infinity + for (const t of hTargets) { + for (const c of hCands) { + const dist = Math.abs(c.value - t.value) + if (dist <= SNAP_PX && dist < bestHDist) { + bestHDist = dist + bestH = { + line: c.value, + orientation: 'h', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + } + } + } + } + + if (bestV) { + const delta = bestV.candidateValue - bestV.targetValue + target.set('left', (target.left ?? 0) + delta) + target.setCoords() + } + if (bestH) { + const delta = bestH.candidateValue - bestH.targetValue + target.set('top', (target.top ?? 0) + delta) + target.setCoords() + } + + this.lastSnapV = bestV + this.lastSnapH = bestH + } + + /* ---------- 渲染 ---------- */ + + private draw(ctx: CanvasRenderingContext2D): void { + const target = this.movingTarget + if (!target) return + + const vt = this.canvas.viewportTransform! + const zoom = this.canvas.getZoom() + const b = target.getBoundingRect() + + // 画布可视范围(逻辑坐标):含页面外的灰色区域。 + // 元素拖出模板(A4)范围时,辅助线也贯穿整个画布、跟着出去。 + const cw = this.canvas.getWidth() + const ch = this.canvas.getHeight() + const viewX0 = -vt[4] / vt[0] + const viewX1 = (cw - vt[4]) / vt[0] + const viewY0 = -vt[5] / vt[3] + const viewY1 = (ch - vt[5]) / vt[3] + + ctx.save() + // 应用视口变换,后续坐标都是画布逻辑坐标 + ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]) + + // 反锯齿 + ctx.imageSmoothingEnabled = false + + // --- 1. 蓝色虚线边界辅助线:沿元素上下左右 4 条边,横穿整个可视画布(含页面外) --- + this.drawBoundary(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + + // --- 2. 红色实线:吸附命中线(贯穿可视画布) --- + // 去重(同一位置可能有多条记录) + const drawn = new Set() + ctx.strokeStyle = COLOR_SNAP + ctx.lineWidth = 1.5 / zoom + for (const r of this.snapResults) { + const key = `${r.orientation}:${r.line.toFixed(3)}` + if (drawn.has(key)) continue + drawn.add(key) + + ctx.beginPath() + if (r.orientation === 'v') { + ctx.moveTo(r.line, viewY0); ctx.lineTo(r.line, viewY1) + } else { + ctx.moveTo(viewX0, r.line); ctx.lineTo(viewX1, r.line) + } + ctx.stroke() + } + + // --- 3. 绿色距离标签(吸附间距 mm) --- + const fontScale = 1 / zoom + ctx.fillStyle = COLOR_DISTANCE + ctx.font = `${Math.max(10, 11 * fontScale)}px "Inter", "PingFang SC", sans-serif` + + const labelDrawn = new Set() + for (const r of this.snapResults) { + const distMm = r.candidateValue - r.targetValue + const absDist = (Math.abs(distMm) / MM_TO_PX).toFixed(1) + const lk = `${r.orientation}:${r.line.toFixed(3)}:${absDist}` + if (labelDrawn.has(lk)) continue + labelDrawn.add(lk) + + let lx: number, ly: number + if (r.orientation === 'v') { + lx = r.line + 4 / zoom + ly = b.top - 4 / zoom + } else { + lx = b.left - 4 / zoom + ly = r.line - 4 / zoom + } + // 确保标签在画布可见范围内 + lx = Math.max(4 / zoom, Math.min(viewX1 - 60 / zoom, lx)) + ly = Math.max(8 / zoom, Math.min(viewY1, ly)) + ctx.fillText(`${absDist} mm`, lx, ly) + } + + // --- 4. 元素实时坐标(左上角) --- + const xMm = (b.left / MM_TO_PX).toFixed(1) + const yMm = (b.top / MM_TO_PX).toFixed(1) + ctx.fillStyle = COLOR_POSITION + ctx.font = `${Math.max(9, 10 * fontScale)}px "Inter", "PingFang SC", sans-serif` + const posLabelX = b.left + 3 / zoom + const posLabelY = b.top - 5 / zoom + ctx.fillText(`X:${xMm} Y:${yMm}`, Math.max(0, posLabelX), Math.max(10 / zoom, posLabelY)) + + // --- 5. 辅助线端点坐标标签(对齐标尺位置,mm) --- + this.drawChips(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + + ctx.restore() + } + + /** 蓝色虚线边界辅助线:沿元素上下左右 4 条边,横穿整个可视画布(含页面外) */ + private drawBoundary( + ctx: CanvasRenderingContext2D, + b: { left: number; top: number; width: number; height: number }, + viewX0: number, viewX1: number, viewY0: number, viewY1: number, + zoom: number, + ): void { + const x0 = b.left + const x1 = b.left + b.width + const y0 = b.top + const y1 = b.top + b.height + ctx.strokeStyle = COLOR_BOUNDING + ctx.lineWidth = 1 / zoom + ctx.setLineDash([4 / zoom, 3 / zoom]) + ctx.beginPath() + // 顶/底边水平线(横穿整画布,对齐水平标尺) + ctx.moveTo(viewX0, y0); ctx.lineTo(viewX1, y0) + ctx.moveTo(viewX0, y1); ctx.lineTo(viewX1, y1) + // 左/右边竖直线(竖穿整画布,对齐垂直标尺) + ctx.moveTo(x0, viewY0); ctx.lineTo(x0, viewY1) + ctx.moveTo(x1, viewY0); ctx.lineTo(x1, viewY1) + ctx.stroke() + ctx.setLineDash([]) + } + + /** 辅助线端点坐标标签(靠近标尺那一头,mm) */ + private drawChips( + ctx: CanvasRenderingContext2D, + b: { left: number; top: number; width: number; height: number }, + viewX0: number, viewX1: number, viewY0: number, viewY1: number, + zoom: number, + ): void { + const x0 = b.left + const x1 = b.left + b.width + const y0 = b.top + const y1 = b.top + b.height + // 放在「靠近标尺那一头」,但必须越过 RulerOverlay 覆盖层(左垂直标尺 / 顶水平标尺各 RULER_THICK px), + // 否则文字会被标尺 DOM 遮住看不见。各偏移 RULER_THICK+4px 正好落在标尺外侧的画布可见区。 + const clampY = (v: number) => Math.max(viewY0 + (RULER_THICK + 6) / zoom, Math.min(viewY1 - 6 / zoom, v)) + const clampX = (v: number) => Math.max(viewX0 + (RULER_THICK + 6) / zoom, Math.min(viewX1 - 6 / zoom, v)) + // 水平线(顶/底)显示 Y,贴在左端、标尺外侧 + this.drawChip(ctx, `Y ${(y0 / MM_TO_PX).toFixed(1)}`, viewX0 + (RULER_THICK + 4) / zoom, clampY(y0), zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + this.drawChip(ctx, `Y ${(y1 / MM_TO_PX).toFixed(1)}`, viewX0 + (RULER_THICK + 4) / zoom, clampY(y1), zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + // 竖直线(左/右)显示 X,贴在顶端、标尺外侧 + this.drawChip(ctx, `X ${(x0 / MM_TO_PX).toFixed(1)}`, clampX(x0), viewY0 + (RULER_THICK + 4) / zoom, zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + this.drawChip(ctx, `X ${(x1 / MM_TO_PX).toFixed(1)}`, clampX(x1), viewY0 + (RULER_THICK + 4) / zoom, zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + } + + /** 常驻辅助线:遍历开启 showGuides 的控件,始终绘制其 4 条边界线 + 坐标标签(不限于拖拽时) */ + private drawPersistent(ctx: CanvasRenderingContext2D): void { + const movingId = this.movingTarget ? (this.movingTarget as unknown as { controlId?: string }).controlId : undefined + const vt = this.canvas.viewportTransform! + const zoom = this.canvas.getZoom() + const cw = this.canvas.getWidth() + const ch = this.canvas.getHeight() + const viewX0 = -vt[4] / vt[0] + const viewX1 = (cw - vt[4]) / vt[0] + const viewY0 = -vt[5] / vt[3] + const viewY1 = (ch - vt[5]) / vt[3] + + for (const obj of this.canvas.getObjects()) { + if (!isPrintObject(obj)) continue + if ((obj as unknown as { showGuides?: boolean }).showGuides !== true) continue + if ((obj as unknown as { controlId?: string }).controlId === movingId) continue // 拖拽中由 draw() 处理,避免重绘 + const b = obj.getBoundingRect() + if (b.width === 0 && b.height === 0) continue + ctx.save() + ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]) + ctx.imageSmoothingEnabled = false + this.drawBoundary(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + this.drawChips(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + ctx.restore() + } + } + + /** 网格视觉线绘制(零依赖,内联到 draw 前调用) */ + private drawGrid(ctx: CanvasRenderingContext2D): void { + if (!this.config.gridVisible) return + const vt = this.canvas.viewportTransform! + const zoom = this.canvas.getZoom() + if (zoom < 0.3) return // 太小不绘,否则糊成一片 + const page = this.getPage() + const step = this.config.gridSizeMm * MM_TO_PX + + ctx.save() + ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]) + ctx.strokeStyle = this.config.gridColor + ctx.lineWidth = 0.5 / zoom + ctx.beginPath() + for (let x = 0; x <= page.width; x += step) { + ctx.moveTo(x, 0) + ctx.lineTo(x, page.height) + } + for (let y = 0; y <= page.height; y += step) { + ctx.moveTo(0, y) + ctx.lineTo(page.width, y) + } + ctx.stroke() + ctx.restore() + } + + /** 圆角矩形路径(Canvas2D 无原生 roundRect 兼容写法) */ + private roundRectPath( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + r: number, + ): void { + ctx.moveTo(x + r, y) + ctx.arcTo(x + w, y, x + w, y + h, r) + ctx.arcTo(x + w, y + h, x, y + h, r) + ctx.arcTo(x, y + h, x, y, r) + ctx.arcTo(x, y, x + w, y, r) + ctx.closePath() + } + + /** 小药丸标签:不透明深色底 + 浅色描边 + 白字,用于辅助线端点显示标尺坐标(mm)。 + * 不透明底色保证在浅色页面与深色画布上都清晰可读;浅描边让药丸边缘在深色背景中也能被分辨。 */ + private drawChip( + ctx: CanvasRenderingContext2D, + text: string, + x: number, + y: number, + zoom: number, + bg: string, + border: string, + fg: string, + ): void { + ctx.save() + ctx.font = `600 ${Math.max(10, 11 / zoom)}px "Inter", "PingFang SC", sans-serif` + ctx.textBaseline = 'middle' + const padX = 4 / zoom + const h = (11 / zoom) + (4 / zoom) + const w = ctx.measureText(text).width + padX * 2 + const top = y - h / 2 + const radius = 3 / zoom + + // 不透明底色 + ctx.beginPath() + this.roundRectPath(ctx, x, top, w, h, radius) + ctx.fillStyle = bg + ctx.fill() + // 浅色描边,确保深色背景下药丸边缘可见 + ctx.lineWidth = 1 / zoom + ctx.strokeStyle = border + ctx.stroke() + + // 文字 + ctx.fillStyle = fg + ctx.fillText(text, x + padX, y) + ctx.restore() + } +} diff --git a/openprint/src/design/canvas/page-gap.spec.ts b/openprint/src/design/canvas/page-gap.spec.ts new file mode 100644 index 0000000..97c1996 --- /dev/null +++ b/openprint/src/design/canvas/page-gap.spec.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest' +import { pageGapPx, modelTopToCanvasY, canvasYToModelTop } from './page-gap' +import { MM_TO_PX } from '@/utils/constants' + +const GAP = 24 +const A4_MM = 297 +const MARGIN_TOP_PX = 10 * MM_TO_PX + +describe('pageGapPx —— 页索引 → 累计间距', () => { + it('首页/负值/零页高 → 0', () => { + expect(pageGapPx(0, A4_MM, GAP)).toBe(0) + expect(pageGapPx(100, A4_MM, GAP)).toBe(0) + expect(pageGapPx(-10, A4_MM, GAP)).toBe(0) + expect(pageGapPx(500, 0, GAP)).toBe(0) + }) + + it('第 2 页起每页累加一个间距', () => { + expect(pageGapPx(300, A4_MM, GAP)).toBe(GAP) + expect(pageGapPx(600, A4_MM, GAP)).toBe(GAP * 2) + expect(pageGapPx(900, A4_MM, GAP)).toBe(GAP * 3) + }) +}) + +describe('modelTopToCanvasY / canvasYToModelTop —— 正反互换', () => { + it('第 1 页控件往返不漂移', () => { + for (const topMm of [0, 5, 100, 296.5]) { + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + } + }) + + it('第 2 页控件含累计间距且往返不漂移', () => { + const topMm = 300 + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + // 第 2 页内容区顶 = (297 + 24) + 页边距,300mm 应在第 2 页内偏下 3mm + expect(y).toBeCloseTo(MARGIN_TOP_PX + 300 * MM_TO_PX + GAP, 6) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + }) + + it('第 3 页控件往返不漂移(两层间距)', () => { + const topMm = 610 + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + }) + + it('页边界附近往返不漂移(迭代收敛)', () => { + for (const topMm of [296.9, 297.1, 593.9, 594.2]) { + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + } + }) +}) diff --git a/openprint/src/design/canvas/page-gap.ts b/openprint/src/design/canvas/page-gap.ts new file mode 100644 index 0000000..1a0d7b3 --- /dev/null +++ b/openprint/src/design/canvas/page-gap.ts @@ -0,0 +1,43 @@ +/** + * 画布页间留白几何纯函数(设计态专用)。 + * + * 模型 mm 保持 flush(与渲染/导出一致),页间距只叠加在画布 px 坐标上: + * - 页 1 顶部 = 页边距;页 i+1 顶部 = i × (页高 + 间距) + 页边距 + * - 正文控件绝对 y = 页边距 + topMm×PX + 累计页间距(按所在页索引) + */ +import { MM_TO_PX } from '@/utils/constants' + +/** 模型 top(mm, flush) 所在页索引 → 该页之前的累计页间距(px);首页恒为 0 */ +export function pageGapPx(topMm: number, pageHeightMm: number, gapPx: number): number { + if (topMm <= 0 || pageHeightMm <= 0) return 0 + const i = Math.floor(topMm / pageHeightMm) + return i > 0 ? i * gapPx : 0 +} + +/** 模型 top(mm, flush) → 画布绝对 y(px):页边距 + 页内偏移 + 累计页间距 */ +export function modelTopToCanvasY( + topMm: number, + marginTopPx: number, + pageHeightMm: number, + gapPx: number, +): number { + return marginTopPx + topMm * MM_TO_PX + pageGapPx(topMm, pageHeightMm, gapPx) +} + +/** 画布绝对 y(px) → 模型 top(mm, flush):反解累计页间距(近页边界最多迭代 2 次收敛) */ +export function canvasYToModelTop( + y: number, + marginTopPx: number, + pageHeightMm: number, + gapPx: number, +): number { + const y0 = y - marginTopPx + let topMm = y0 / MM_TO_PX + let gap = pageGapPx(topMm, pageHeightMm, gapPx) + if (gap > 0) { + topMm = (y0 - gap) / MM_TO_PX + const gap2 = pageGapPx(topMm, pageHeightMm, gapPx) + if (gap2 !== gap) topMm = (y0 - gap2) / MM_TO_PX + } + return topMm +} diff --git a/openprint/src/design/canvas/page-geometry.spec.ts b/openprint/src/design/canvas/page-geometry.spec.ts new file mode 100644 index 0000000..64a0b2a --- /dev/null +++ b/openprint/src/design/canvas/page-geometry.spec.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { computePhysicalPageCount } from './page-geometry' + +describe('computePhysicalPageCount', () => { + const A4 = 297 + + it('无内容时至少 1 页', () => { + expect(computePhysicalPageCount(0, A4)).toBe(1) + expect(computePhysicalPageCount(-50, A4)).toBe(1) + }) + + it('内容落在第 1 页内 → 1 页', () => { + expect(computePhysicalPageCount(100, A4)).toBe(1) + expect(computePhysicalPageCount(296, A4)).toBe(1) + }) + + it('内容越过第 1 页底部 → 第 2 页', () => { + expect(computePhysicalPageCount(298, A4)).toBe(2) + expect(computePhysicalPageCount(400, A4)).toBe(2) + }) + + it('内容延伸到第 3 页', () => { + expect(computePhysicalPageCount(594, A4)).toBe(3) + expect(computePhysicalPageCount(800, A4)).toBe(3) + }) + + it('页高为 0 时安全返回 1', () => { + expect(computePhysicalPageCount(500, 0)).toBe(1) + }) +}) diff --git a/openprint/src/design/canvas/page-geometry.ts b/openprint/src/design/canvas/page-geometry.ts new file mode 100644 index 0000000..9168d37 --- /dev/null +++ b/openprint/src/design/canvas/page-geometry.ts @@ -0,0 +1,11 @@ +/** + * 画布多页几何纯函数(与渲染端 layoutStaticOnly 的落页口径一致)。 + * + * 正文控件按 `marginTop + top` 绝对定位(top 为相对内容区 mm), + * 越过内容区底部即落入下一页。物理页数 = floor(maxBottomMm / pageHeightMm) + 1。 + */ +export function computePhysicalPageCount(maxBottomMm: number, pageHeightMm: number): number { + if (pageHeightMm <= 0) return 1 + if (maxBottomMm <= 0) return 1 + return Math.max(1, Math.floor(maxBottomMm / pageHeightMm) + 1) +} diff --git a/openprint/src/design/canvas/rulers/RulerOverlay.vue b/openprint/src/design/canvas/rulers/RulerOverlay.vue new file mode 100644 index 0000000..a77b6b1 --- /dev/null +++ b/openprint/src/design/canvas/rulers/RulerOverlay.vue @@ -0,0 +1,211 @@ + + + diff --git a/openprint/src/design/canvas/rulers/rulerHighlight.ts b/openprint/src/design/canvas/rulers/rulerHighlight.ts new file mode 100644 index 0000000..bce7062 --- /dev/null +++ b/openprint/src/design/canvas/rulers/rulerHighlight.ts @@ -0,0 +1,24 @@ +/** + * rulerHighlight —— 标尺高亮带共享状态 + * + * SmartGuides 在 object:moving / object:scaling / selection 事件里, + * 把「当前选中/拖拽元素的包围盒(画布逻辑 px = 页面 px,未含缩放/平移)」 + * 写入这个模块级 ref;RulerOverlay 读取后按 viewport 映射成标尺上的彩色高亮带 + * (顶标尺带 = 元素宽度,左标尺带 = 元素高度),随组件移动丝滑滑动。 + * + * 用模块级 ref 而非 Pinia store,是为了避免把画布引擎模块与组件 store 耦合。 + */ +import { ref } from 'vue' + +export interface RulerBand { + /** 左边缘(画布逻辑 px) */ + left: number + /** 上边缘(画布逻辑 px) */ + top: number + /** 宽(画布逻辑 px) */ + width: number + /** 高(画布逻辑 px) */ + height: number +} + +export const rulerBand = ref(null) diff --git a/openprint/src/design/canvas/table-design-render.spec.ts b/openprint/src/design/canvas/table-design-render.spec.ts new file mode 100644 index 0000000..c3536c0 --- /dev/null +++ b/openprint/src/design/canvas/table-design-render.spec.ts @@ -0,0 +1,172 @@ +/** + * 设计期表格 HTML 渲染单测(table-design-render.ts) + * + * 覆盖:renderTableGridHtml 标记(data-row/data-col、类名、占位符、纵向合并)、 + * gridColXs / gridRowHeights / computeGridLayout 几何、hitTestCell 命中测试。 + * 重点确保"设计所见"与运行期类名一致,且无 cells 的旧模板能正常回落。 + */ +import { describe, expect, it } from 'vitest' +import type { TableControl } from '@/types/control' +import { + computeGridLayout, + gridColXs, + gridRowHeights, + hitTestCell, + renderTableGridHtml, +} from './table-design-render' +import { seedSummaryTail } from '@/core/layout-engine/table-cells' + +function baseTable(over: Partial = {}): TableControl { + return { + id: 't1', + type: 'table', + left: 0, + top: 0, + width: 180, + height: 60, + columns: [ + { title: '序号', expression: '{{rowIndex + 1}}', width: 15, align: 'center', headerAlign: 'center' }, + { title: '名称', field: 'name', width: 60 }, + { title: '数量', field: 'qty', width: 25, align: 'right', headerAlign: 'center' }, + ], + options: { borders: 'all', verticalAlign: 'middle' }, + ...over, + } +} + +describe('renderTableGridHtml', () => { + it('数据表:输出表格根类 op-table + 运行期同款修饰类', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(html).toContain('class="op-node op-table b-all va-middle ts-none"') + expect(html).toContain('table-layout:fixed') + }) + + it('边框修复:不再内联 border-collapse:collapse(否则被 overlay overflow:hidden 切掉右/下边)', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items' })) + expect(html).not.toContain('border-collapse:collapse') + // 外框走 tableCss 的 separate + 末行/末列补边,四边始终完整 + expect(html).toContain('table-layout:fixed') + }) + + it('样式预设:默认 none 挂 ts-none,指定后挂对应类', () => { + const def = renderTableGridHtml(baseTable({ dataSource: 'items', options: { borders: 'all', verticalAlign: 'middle' } })) + expect(def).toContain('ts-none') + const zebra = renderTableGridHtml(baseTable({ dataSource: 'items', options: { borders: 'all', verticalAlign: 'middle', tableStyle: 'zebra' } })) + expect(zebra).toContain('ts-zebra') + }) + + it('聚合尾行对齐运行期类名:本页合计→is-subtotal,总计/大写金额→is-summary', () => { + const t = seedSummaryTail( + baseTable({ + dataSource: 'items', + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '数量', field: 'items[].qty', width: 25, align: 'right', headerAlign: 'center' }, + ], + data: [{ name: 'A', qty: 2 }], + }), + { numericColumns: [1], moneyColumn: 1, capital: true }, + ) + const html = renderTableGridHtml(t) + expect(html).toContain('is-subtotal') + expect(html).toContain('is-summary') + }) + + it('每个 td 带 data-row/data-col,便于 overlay 编辑写回', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items' })) + expect(html).toContain('data-row="0"') + expect(html).toContain('data-col="2"') + }) + + it('行类与语义匹配:表头 / 数据样例行 / 静态', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(html).toContain('class="is-header"') + expect(html).toContain('class="is-data is-template"') + expect(html).toContain('class="is-static"') + }) + + it('绑定的数据样例行:渲染为字段占位符而非空', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items' })) + expect(html).toContain('{{item.name}}') + expect(html).toContain('{{item.qty}}') + // 表头占位符走 expression + expect(html).toContain('{{rowIndex + 1}}') + }) + + it('静态文字行:原样输出字面量', () => { + const cells = [ + [{ text: '列1' }, { text: '列2' }, { text: '列3' }], + [{ field: 'a' }, { field: 'b' }, { field: 'c' }], + [{ text: '合计:' }, {}, {}], + ] + const html = renderTableGridHtml(baseTable({ dataSource: 'items', cells, staticRows: 1 })) + expect(html).toContain('合计:') + expect(html).toContain('列1') + }) + + it('空单元格输出
占位,避免高度塌陷', () => { + // 布局网格(无 dataSource)多数静态格为空 → 输出
+ const html = renderTableGridHtml(baseTable({ designRows: 3 })) + expect(html).toContain('
') + }) + + it('横向合并:输出 colspan 属性', () => { + const t = baseTable({ dataSource: 'items' }) + const cells = [ + [{ text: 'H' }, { text: '' }, { text: '' }], + [{ field: 'a', colSpan: 2 }, { text: '' }, { field: 'c' }], + ] + const html = renderTableGridHtml({ ...t, cells, staticRows: 0 }) + expect(html).toContain('colspan="2"') + }) +}) + +describe('几何布局', () => { + it('gridColXs:长度 colCount+1,末值=表格宽,单调递增', () => { + const xs = gridColXs(baseTable({ dataSource: 'items' })) + expect(xs.length).toBe(4) + expect(xs[0]).toBe(0) + expect(xs[3]).toBe(180) + expect(xs[1]! < xs[2]!).toBe(true) + }) + + it('gridRowHeights:返回 rowCount 个正值', () => { + const hs = gridRowHeights(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(hs.length).toBe(3) + expect(hs.every((h) => h > 0)).toBe(true) + }) + + it('computeGridLayout:汇总 colXs/rowYs/rowCount/colCount', () => { + const g = computeGridLayout(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(g.colCount).toBe(3) + expect(g.rowCount).toBe(3) + expect(g.colXs.length).toBe(4) + expect(g.rowYs.length).toBe(4) + expect(g.rowYs[0]).toBe(0) + }) +}) + +describe('hitTestCell', () => { + const layout = computeGridLayout(baseTable({ dataSource: 'items', staticRows: 1 })) + + it('左上角命中第 0 行第 0 列', () => { + expect(hitTestCell(layout, 0.01, 0.01)).toEqual({ row: 0, col: 0 }) + }) + + it('右下角命中末行末列', () => { + const hit = hitTestCell(layout, 0.99, 0.99) + expect(hit).toEqual({ row: layout.rowCount - 1, col: layout.colCount - 1 }) + }) + + it('越界(frac 超出 [0,1])返回 null', () => { + expect(hitTestCell(layout, -0.1, 0.5)).toBeNull() + expect(hitTestCell(layout, 0.5, 1.2)).toBeNull() + }) + + it('落在第 2 列区间命中 col=1', () => { + // colXs 近似 [0, ~25, ~136, 180],中间点应落第 1 列 + const x = (layout.colXs[1]! + layout.colXs[2]!) / 2 / layout.colXs[3]! + const hit = hitTestCell(layout, x, 0.01) + expect(hit?.col).toBe(1) + }) +}) diff --git a/openprint/src/design/canvas/table-design-render.ts b/openprint/src/design/canvas/table-design-render.ts new file mode 100644 index 0000000..bc1076b --- /dev/null +++ b/openprint/src/design/canvas/table-design-render.ts @@ -0,0 +1,194 @@ +/** + * table-design-render —— 设计期表格 HTML 渲染(方案 A:与运行期共用渲染语义) + * + * 设计期表格以 HTML overlay 呈现(绝对定位、transform 同步 Fabric 节点 + 视口), + * 由本模块产出的 HTML 与运行期 html-renderer 使用同一套样式类 / 内联样式键, + * 从而做到"设计即打印"的真·所见即所得;双击单元格即可原生 contenteditable 编辑。 + * + * 所有几何以 mm 表达(CSS 原生支持 mm 单位),由调用方统一做 zoom/scale 变换。 + */ +import type { TableCell, TableControl, TableColumn } from '@/types/control' +import { + buildDesignGrid, + computeSpanLayout, + designRowHeights, + resolveCellStyleFor, + type DesignRowKind, +} from '@/core/layout-engine/table-cells' +import { isAggToken, parseAggToken } from '@/core/layout-engine/aggregate' +import { normalizeColumnWidths } from '@/core/layout-engine/table-engine' +import { diagonalBackground } from '@/core/renderer-html/css-generator' + +const PLACEHOLDER = '#9aa0a6' + +/** + * 各可视行的行高(mm),与渲染、命中测试共用。 + * 实现下沉到 core(designRowHeights),**运行期布局网格用的是同一份算法** —— 这样 + * 空白表格在画布上什么样,打印出来就是什么样。 + */ +export function gridRowHeights(control: TableControl): number[] { + return designRowHeights(control) +} + +/** 列累计 x(mm,长度 colCount+1,末值 = 表格宽) */ +export function gridColXs(control: TableControl): number[] { + const widths = normalizeColumnWidths(control.columns, control.width) + const xs = [0] + for (const w of widths) xs.push(xs[xs.length - 1]! + w) + return xs +} + +export interface GridLayout { + colXs: number[] + rowYs: number[] + rowCount: number + colCount: number +} + +/** 设计期网格布局(mm),供渲染与双击命中测试共用 */ +export function computeGridLayout(control: TableControl): GridLayout { + const grid = buildDesignGrid(control) + const colXs = gridColXs(control) + const rowHs = gridRowHeights(control) + const rowYs = [0] + for (const h of rowHs) rowYs.push(rowYs[rowYs.length - 1]! + h) + return { colXs, rowYs, rowCount: grid.rowCount, colCount: grid.colCount } +} + +/** 计算双击点命中的单元格(fraction ∈ [0,1]) */ +export function hitTestCell( + layout: GridLayout, + fracX: number, + fracY: number, +): { row: number; col: number } | null { + if (fracX < 0 || fracX > 1 || fracY < 0 || fracY > 1) return null + const x = fracX * layout.colXs[layout.colXs.length - 1]! + const y = fracY * layout.rowYs[layout.rowYs.length - 1]! + let col = 0 + for (let c = 1; c < layout.colXs.length; c++) { + if (x <= layout.colXs[c]!) { col = c - 1; break } + col = c - 1 + } + let row = 0 + for (let r = 1; r < layout.rowYs.length; r++) { + if (y <= layout.rowYs[r]!) { row = r - 1; break } + row = r - 1 + } + return { row, col } +} + +function esc(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>') +} + +/** 字段 → 占位符显示:含数组标记(items[].name)直接用路径,避免 {{item.items[].name}} 冗余前缀 */ +function fieldPlaceholder(field: string): string { + return field.includes('[]') ? `{{${field}}}` : `{{item.${field}}}` +} + +function placeholderOf(cell: TableCell, col: TableColumn | undefined): string { + if (cell.expression) return cell.expression + if (cell.field) return fieldPlaceholder(cell.field) + if (col?.expression) return col.expression + if (col?.field) return fieldPlaceholder(col.field) + return '' +} + +/** + * 单元格内联样式。 + * 只输出**显式设置过**的属性,未设置的交给 css-generator 的 `.op-table` 类规则兜底 + * (例如表头加粗、va-* 垂直对齐),保证设计期与运行期视觉一致。 + */ +function cellStyle( + control: TableControl, + cell: TableCell, + col: TableColumn | undefined, + placeholder: boolean, + kind: DesignRowKind, +): string { + const s = resolveCellStyleFor(control, col, cell, kind) + const parts: string[] = [] + if (s.fontSize) parts.push(`font-size:${s.fontSize}pt`) + if (s.fontFamily) parts.push(`font-family:${s.fontFamily}`) + if (s.bold !== undefined) parts.push(`font-weight:${s.bold ? 'bold' : 'normal'}`) + if (s.italic !== undefined) parts.push(`font-style:${s.italic ? 'italic' : 'normal'}`) + if (s.underline !== undefined) parts.push(`text-decoration:${s.underline ? 'underline' : 'none'}`) + if (s.align) parts.push(`text-align:${s.align}`) + if (s.valign) parts.push(`vertical-align:${s.valign}`) + if (s.backgroundColor) parts.push(`background-color:${s.backgroundColor}`) + if (s.diagonal) parts.push(diagonalBackground(s.diagonal)) + if (placeholder) parts.push(`color:${PLACEHOLDER}`) + else if (s.color) parts.push(`color:${s.color}`) + return parts.join(';') +} + +/** + * 渲染设计期表格 HTML(表头 / 数据样例行 / 静态尾行)。 + * + * 类名与运行期 html-renderer 完全一致(op-table / b-* / va-* / is-header / is-data), + * 因此同一份 CSS 即可驱动两端 —— 这正是"设计即打印"的根。 + * 每个 td 带 `data-row` / `data-col`,供 overlay 做 DOM 级单元格定位与编辑写回。 + */ +export function renderTableGridHtml(control: TableControl): string { + const grid = buildDesignGrid(control) + const opts = control.options ?? {} + const borders = opts.borders ?? 'all' + const va = opts.verticalAlign ?? 'middle' + const colXs = gridColXs(control) + const colWs = colXs.slice(1).map((_, i) => colXs[i + 1]! - colXs[i]!) + const rowHs = gridRowHeights(control) + // 合并布局(colSpan + rowSpan 一起算),与设计期渲染、运行期引擎共用同一份结果 + const spanLayout = computeSpanLayout(grid.cells, grid.rowCount, grid.colCount) + + const cols = colWs.map((w) => ``).join('') + const rows: string[] = [] + + for (let r = 0; r < grid.rowCount; r++) { + const isHeaderRow = r < grid.headerRows + const isDataRow = grid.isData && r === grid.headerRows + const kind: DesignRowKind = isHeaderRow ? 'header' : isDataRow ? 'data' : 'static' + const spanRow = spanLayout[r]! + const cells: string[] = [] + let c = 0 + while (c < grid.colCount) { + if (spanRow[c]!.skip) { + c++ + continue + } + const cell = grid.cells[r]?.[c] ?? {} + const col = control.columns[c] + const span = spanRow[c]!.colSpan + const rspan = spanRow[c]!.rowSpan + const isAgg = isAggToken(cell.text) + const bound = Boolean(cell.field || cell.expression || (isDataRow && col?.field) || (isDataRow && col?.expression)) + // 聚合 token({{#totalSum}} 等)按占位符着色显示,提示用户这是动态计算单元格 + const placeholder = bound || isAgg + const text = bound ? placeholderOf(cell, col) : (cell.text ?? '') + const style = cellStyle(control, cell, col, placeholder, kind) + const spanAttr = span > 1 ? ` colspan="${span}"` : '' + const rspanAttr = rspan > 1 ? ` rowspan="${rspan}"` : '' + const tdClass = isAgg ? ' is-agg' : '' + const styleAttr = style ? ` style="${style}"` : '' + cells.push( + `${esc(text) || '
'}`, + ) + c += span + } + const rh = rowHs[r] + let cls = isDataRow ? 'is-data is-template' : isHeaderRow ? 'is-header' : 'is-static' + // 聚合尾行(本页合计/总计/大写金额)对齐运行期 is-subtotal/is-summary 类,保证样式预设两端一致 + if (!isHeaderRow && !isDataRow) { + const tokens = (grid.cells[r] ?? []).map((c) => parseAggToken(c.text)) + if (tokens.some((t) => t === 'pageSum' || t === 'pageAvg' || t === 'pageCount')) cls = 'is-subtotal' + else if (tokens.some((t) => t === 'totalSum' || t === 'totalAvg' || t === 'totalCount' || t === 'pageCap' || t === 'totalCap')) cls = 'is-summary' + } + rows.push(`${cells.join('')}`) + } + + const tableStyle = opts.tableStyle ?? 'none' + return ( + `` + + `${cols}${rows.join('')}
` + ) +} diff --git a/openprint/src/design/canvas/table-style-presets.ts b/openprint/src/design/canvas/table-style-presets.ts new file mode 100644 index 0000000..4a17e02 --- /dev/null +++ b/openprint/src/design/canvas/table-style-presets.ts @@ -0,0 +1,45 @@ +/** + * 表格样式预设元数据 —— 设计期「样式库」弹窗与属性面板共用的唯一真相源。 + * + * 每个预设对应 css-generator.ts 里的一组 `.op-table.ts-` 规则; + * 渲染端(table-design-render.ts / html-renderer.ts)把同名 class 挂在 上, + * 因此画布、预览、PDF、样式库预览四端视觉完全一致。 + * + * 预设不只是"配色"——`borders` 可捆绑边框框线方案(如三线表 / 外框 / 全网格), + * 点击即同时套用配色与框线,类似 Excel 把"表格样式"与"框线"打包切换。 + */ +import type { TableStylePreset } from '@/types/control' + +export interface TableStyleMeta { + /** 对应
上的 ts- class 与 TableOptions.tableStyle 取值 */ + key: TableStylePreset + /** 展示名(按钮 / 卡片标题) */ + label: string + /** 一句话描述,帮助快速区分相近样式 */ + desc: string + /** 可选:捆绑的边框框线方案;不填则只改配色、保留当前边框 */ + borders?: 'all' | 'none' | 'horizontal' | 'outline' | 'three-line' +} + +/** Excel 式表格样式库(按"从简到繁 / 配色 → 框线"排列) */ +export const TABLE_STYLE_PRESETS: TableStyleMeta[] = [ + { key: 'none', label: '无样式', desc: '仅边框 + 表头加粗,无任何背景' }, + { key: 'header', label: '表头高亮', desc: '浅灰表头' }, + { key: 'header-dark', label: '深蓝表头', desc: '深蓝底白字' }, + { key: 'header-green', label: '绿色表头', desc: '绿底白字' }, + { key: 'zebra', label: '斑马纹', desc: '浅蓝交替行' }, + { key: 'zebra-blue', label: '蓝纹', desc: '深蓝交替行' }, + { key: 'zebra-gray', label: '灰纹', desc: '灰色交替行' }, + { key: 'three-segment', label: '三段式', desc: '表头 + 合计强调' }, + { key: 'banded-cols', label: '隔列着色', desc: '奇偶列交替底' }, + // —— 框线类(配色 + 边框打包) —— + { key: 'grid', label: '全网格', desc: '全框线 + 无配色', borders: 'all' }, + { key: 'minimal', label: '极简外框', desc: '仅外框、无内线', borders: 'outline' }, + { key: 'report', label: '三线表', desc: '顶/表头底/底线,无内线(报表风)', borders: 'three-line' }, + { key: 'timetable', label: '课表', desc: '全网格 + 表头高亮(角标斜线见单元格)', borders: 'all' }, +] + +/** 预设 key → 展示名(未知值回落「无样式」) */ +export function tableStyleLabel(key: TableStylePreset | undefined | null): string { + return TABLE_STYLE_PRESETS.find((p) => p.key === key)?.label ?? '无样式' +} diff --git a/openprint/src/design/canvas/zoom.ts b/openprint/src/design/canvas/zoom.ts new file mode 100644 index 0000000..72ef524 --- /dev/null +++ b/openprint/src/design/canvas/zoom.ts @@ -0,0 +1,15 @@ +/** + * 缩放工具函数(CanvasDesigner 的纯函数部分,供工具栏/快捷键复用) + */ +import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from '@/utils/constants' + +export const clampZoom = (z: number): number => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, z)) + +export const nextZoomIn = (z: number): number => clampZoom(Math.round((z + ZOOM_STEP) * 100) / 100) +export const nextZoomOut = (z: number): number => clampZoom(Math.round((z - ZOOM_STEP) * 100) / 100) + +/** 缩放百分比显示(100% / 125% ...) */ +export const zoomLabel = (z: number): string => `${Math.round(z * 100)}%` + +/** 常用缩放下拉档位 */ +export const ZOOM_PRESETS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4] as const diff --git a/openprint/src/design/composables/useConfirm.ts b/openprint/src/design/composables/useConfirm.ts new file mode 100644 index 0000000..0560213 --- /dev/null +++ b/openprint/src/design/composables/useConfirm.ts @@ -0,0 +1,48 @@ +/** + * useConfirm —— 统一的「确认」弹窗(应用内系统弹窗) + * + * 取代浏览器原生 window.confirm / window.alert: + * - 原生 confirm 是浏览器 Chrome 级别的 UI,与项目主题/设计语言割裂,且阻塞线程; + * - 本封装基于 naive-ui 的 NDialogProvider(App.vue 已挂载 ), + * 弹窗随亮/暗主题自适应,文案与按钮可定制,体验一致。 + * + * 用法(必须在组件 + + + + diff --git a/openprint/src/design/modals/JsonViewerModal.vue b/openprint/src/design/modals/JsonViewerModal.vue new file mode 100644 index 0000000..b158d2f --- /dev/null +++ b/openprint/src/design/modals/JsonViewerModal.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/openprint/src/design/modals/PrintDialog.vue b/openprint/src/design/modals/PrintDialog.vue new file mode 100644 index 0000000..34ddaa5 --- /dev/null +++ b/openprint/src/design/modals/PrintDialog.vue @@ -0,0 +1,635 @@ + + + + + diff --git a/openprint/src/design/modals/SettingsModal.vue b/openprint/src/design/modals/SettingsModal.vue new file mode 100644 index 0000000..531b112 --- /dev/null +++ b/openprint/src/design/modals/SettingsModal.vue @@ -0,0 +1,534 @@ + + + + + diff --git a/openprint/src/design/modals/TableStylePickerModal.vue b/openprint/src/design/modals/TableStylePickerModal.vue new file mode 100644 index 0000000..2358b20 --- /dev/null +++ b/openprint/src/design/modals/TableStylePickerModal.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/openprint/src/design/modals/TemplateMarket.vue b/openprint/src/design/modals/TemplateMarket.vue new file mode 100644 index 0000000..8abdd6e --- /dev/null +++ b/openprint/src/design/modals/TemplateMarket.vue @@ -0,0 +1,212 @@ + + + + + + + diff --git a/openprint/src/design/modals/TemplateModal.vue b/openprint/src/design/modals/TemplateModal.vue new file mode 100644 index 0000000..7253ee8 --- /dev/null +++ b/openprint/src/design/modals/TemplateModal.vue @@ -0,0 +1,210 @@ + + + diff --git a/openprint/src/design/panels/ControlLibrary.vue b/openprint/src/design/panels/ControlLibrary.vue new file mode 100644 index 0000000..6471287 --- /dev/null +++ b/openprint/src/design/panels/ControlLibrary.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/openprint/src/design/panels/DataSourceTree.vue b/openprint/src/design/panels/DataSourceTree.vue new file mode 100644 index 0000000..a463813 --- /dev/null +++ b/openprint/src/design/panels/DataSourceTree.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/openprint/src/design/panels/LayerPanel.vue b/openprint/src/design/panels/LayerPanel.vue new file mode 100644 index 0000000..b269854 --- /dev/null +++ b/openprint/src/design/panels/LayerPanel.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/openprint/src/design/panels/LeftPanel.vue b/openprint/src/design/panels/LeftPanel.vue new file mode 100644 index 0000000..654f473 --- /dev/null +++ b/openprint/src/design/panels/LeftPanel.vue @@ -0,0 +1,28 @@ + + + diff --git a/openprint/src/design/panels/RightPanel.vue b/openprint/src/design/panels/RightPanel.vue new file mode 100644 index 0000000..1cfc68e --- /dev/null +++ b/openprint/src/design/panels/RightPanel.vue @@ -0,0 +1,365 @@ + + + + + diff --git a/openprint/src/design/panels/props/BindingEditor.vue b/openprint/src/design/panels/props/BindingEditor.vue new file mode 100644 index 0000000..95cb898 --- /dev/null +++ b/openprint/src/design/panels/props/BindingEditor.vue @@ -0,0 +1,45 @@ + + + diff --git a/openprint/src/design/panels/props/ChartProps.vue b/openprint/src/design/panels/props/ChartProps.vue new file mode 100644 index 0000000..0cc062c --- /dev/null +++ b/openprint/src/design/panels/props/ChartProps.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/openprint/src/design/panels/props/CodeProps.vue b/openprint/src/design/panels/props/CodeProps.vue new file mode 100644 index 0000000..5c76685 --- /dev/null +++ b/openprint/src/design/panels/props/CodeProps.vue @@ -0,0 +1,100 @@ + + + diff --git a/openprint/src/design/panels/props/CommonProps.vue b/openprint/src/design/panels/props/CommonProps.vue new file mode 100644 index 0000000..e1434e2 --- /dev/null +++ b/openprint/src/design/panels/props/CommonProps.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/openprint/src/design/panels/props/ImageProps.vue b/openprint/src/design/panels/props/ImageProps.vue new file mode 100644 index 0000000..bb7c9bf --- /dev/null +++ b/openprint/src/design/panels/props/ImageProps.vue @@ -0,0 +1,105 @@ + + + diff --git a/openprint/src/design/panels/props/RichTextEditor.vue b/openprint/src/design/panels/props/RichTextEditor.vue new file mode 100644 index 0000000..e4c9527 --- /dev/null +++ b/openprint/src/design/panels/props/RichTextEditor.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/openprint/src/design/panels/props/RichTextProps.vue b/openprint/src/design/panels/props/RichTextProps.vue new file mode 100644 index 0000000..dfa403b --- /dev/null +++ b/openprint/src/design/panels/props/RichTextProps.vue @@ -0,0 +1,56 @@ + + + diff --git a/openprint/src/design/panels/props/ShapeProps.vue b/openprint/src/design/panels/props/ShapeProps.vue new file mode 100644 index 0000000..1aa20c7 --- /dev/null +++ b/openprint/src/design/panels/props/ShapeProps.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/openprint/src/design/panels/props/TableProps.vue b/openprint/src/design/panels/props/TableProps.vue new file mode 100644 index 0000000..7864f7e --- /dev/null +++ b/openprint/src/design/panels/props/TableProps.vue @@ -0,0 +1,797 @@ + + + diff --git a/openprint/src/design/panels/props/TextProps.vue b/openprint/src/design/panels/props/TextProps.vue new file mode 100644 index 0000000..2dcd3e7 --- /dev/null +++ b/openprint/src/design/panels/props/TextProps.vue @@ -0,0 +1,312 @@ + + + diff --git a/openprint/src/design/panels/props/ZoneProps.vue b/openprint/src/design/panels/props/ZoneProps.vue new file mode 100644 index 0000000..60441e1 --- /dev/null +++ b/openprint/src/design/panels/props/ZoneProps.vue @@ -0,0 +1,50 @@ + + + diff --git a/openprint/src/design/preview/PreviewPanel.vue b/openprint/src/design/preview/PreviewPanel.vue new file mode 100644 index 0000000..84f58ad --- /dev/null +++ b/openprint/src/design/preview/PreviewPanel.vue @@ -0,0 +1,416 @@ + + +