feat(preview): render Excel workbooks in the browser

This commit is contained in:
yudshj committed 2026-09-21 13:20:44 +08:00
1 parent eaf0f68bb2
commit 087dc65e89
42 files changed
+1666 -40

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-node-office-kit.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-node-office-kit.md
2026-09-11-node-office-kit.md: 132ac8b3dd709f9f16254a5569dec59f01d29fda 2026-09-11-node-office-kit.md: 9042e23b674d208af19b0829f4c6ec7961f2464c
2026-09-11-node-office-kit.zh.md: c7bdab2322766c42cfee4092f158d7b6b530e875 2026-09-11-node-office-kit.zh.md: 2707b26ae1a25e8d8819142567201e28a768346a
@@ -14,7 +14,7 @@ The [office-to-pdf capability](../../../../packages/document/README.md) delegate
The [platform engine decision](2026-09-15-platform-office-engines.md) requires the kit’s declared native target engine, or WASM when no native target is declared. Missing or invalid required engines reject conversion. The shared [bounded provider](2026-09-15-bounded-office-conversion.md) owns admission, conversion reuse, and cancellation through scratch cleanup. Preview consumes that provider without registering another converter or requiring Office authoring skills. The [platform engine decision](2026-09-15-platform-office-engines.md) requires the kit’s declared native target engine, or WASM when no native target is declared. Missing or invalid required engines reject conversion. The shared [bounded provider](2026-09-15-bounded-office-conversion.md) owns admission, conversion reuse, and cancellation through scratch cleanup. Preview consumes that provider without registering another converter or requiring Office authoring skills.
The service and Remote methods accept DOC, DOCX, XLS, XLSX, PPT, and PPTX. The Client preview registers DOC, DOCX, PPT, and PPTX; XLS and XLSX use the unsupported-preview state and can open in the system default application without requesting conversion. The kit verifies bounded ZIP membership and content types for OOXML inputs and the OLE compound-file header for binary Office inputs before LibreOffice imports them. Renaming text to an Office suffix does not admit it. Binary formats return no missing-font diagnostics because the kit does not extract their font tables. It writes a fresh, exclusively created PDF in a caller-owned private directory. DSH reads and validates the complete output before removing scratch files. The [service’s Remote method](../../../../packages/document/office-to-pdf/README.md) authorizes source access through [Workspace Files](2026-09-09-workspace-file-read-authority.md), preserves the source path/version, and returns PDF bytes. Source read caps and generated PDF caps remain independent. One source path/version snapshot governs the access probe and deferred read, including size-failure rechecks, so conversion cannot publish bytes under another identity. Preview bytes do not enter Session storage or persistent caches. The service and Remote methods accept DOC, DOCX, XLS, XLSX, PPT, and PPTX. Client PDF previews use this path for Word and PowerPoint; [browser Excel preview](../feature/2026-09-16-browser-excel-preview.md) reads XLSX independently. The kit verifies bounded ZIP membership and content types for OOXML inputs and the OLE compound-file header for binary Office inputs before LibreOffice imports them. Renaming text to an Office suffix does not admit it. Binary formats return no missing-font diagnostics because the kit does not extract their font tables. It writes a fresh, exclusively created PDF in a caller-owned private directory. DSH reads and validates the complete output before removing scratch files. The [service’s Remote method](../../../../packages/document/office-to-pdf/README.md) authorizes source access through [Workspace Files](2026-09-09-workspace-file-read-authority.md), preserves the source path/version, and returns PDF bytes. Source read caps and generated PDF caps remain independent. One source path/version snapshot governs the access probe and deferred read, including size-failure rechecks, so conversion cannot publish bytes under another identity. Preview bytes do not enter Session storage or persistent caches.
A converter reuses font metadata obtained by its first conversion Worker; original font buffers and decoded glyph coverage remain conversion-local. Workers validate indexed files when reading them. Exact installed families precede configured alternatives, and complete family/style/weight/italic/width/pitch/language/code-point requests retain their distinct matches. WASM callbacks import original font files, including complete collections, into MEMFS. Native engines also retain their platform's font discovery. Neither path downloads or installs fonts; native OS-managed font memory is outside the explicit import budget. Recreating the converter refreshes its metadata after font changes. A converter reuses font metadata obtained by its first conversion Worker; original font buffers and decoded glyph coverage remain conversion-local. Workers validate indexed files when reading them. Exact installed families precede configured alternatives, and complete family/style/weight/italic/width/pitch/language/code-point requests retain their distinct matches. WASM callbacks import original font files, including complete collections, into MEMFS. Native engines also retain their platform's font discovery. Neither path downloads or installs fonts; native OS-managed font memory is outside the explicit import budget. Recreating the converter refreshes its metadata after font changes.
@@ -14,7 +14,7 @@ Status: implemented
[平台引擎决策](2026-09-15-platform-office-engines.zh.md)要求使用 kit 已声明的原生目标引擎,未声明原生目标时使用 WASM。缺失或无效的必需引擎会拒绝转换。共享的[有界提供方](2026-09-15-bounded-office-conversion.zh.md)负责准入、转换复用以及持续到临时文件清理完成的取消。预览消费该提供方,不注册另一个转换器,也不依赖 Office 创作 skills。 [平台引擎决策](2026-09-15-platform-office-engines.zh.md)要求使用 kit 已声明的原生目标引擎,未声明原生目标时使用 WASM。缺失或无效的必需引擎会拒绝转换。共享的[有界提供方](2026-09-15-bounded-office-conversion.zh.md)负责准入、转换复用以及持续到临时文件清理完成的取消。预览消费该提供方,不注册另一个转换器,也不依赖 Office 创作 skills。
服务与 Remote 方法接受 DOC、DOCX、XLS、XLSX、PPT 和 PPTX。Client 预览注册 DOC、DOCX、PPT 和 PPTX;XLS 与 XLSX 使用不支持预览的空状态,可由系统默认应用打开,并且不请求转换。LibreOffice 导入前,kit 校验 OOXML 输入的有界 ZIP 成员和内容类型,以及二进制 Office 输入的 OLE 复合文件头。将文本改为 Office 后缀不能通过校验。kit 不提取二进制格式的字体表,因此这些格式不返回缺失字体诊断。kit 在调用方拥有的私有目录中独占创建新的 PDF。DSH 读取并校验完整输出后才删除临时文件。[服务的 Remote 方法](../../../../packages/document/office-to-pdf/README.zh.md)通过 [Workspace Files](2026-09-09-workspace-file-read-authority.zh.md)授权源文件访问,保留源路径和版本,并返回 PDF 字节。源文件读取上限与生成 PDF 上限相互独立。读取权限探测和延迟读取(包括超限失败后的复查)采用同一个源路径/版本快照,防止转换将字节发布到另一个源身份下。预览字节不会进入 Session 存储或持久缓存。 服务和 Remote 方法接受 DOC、DOCX、XLS、XLSX、PPT 和 PPTX。Client PDF 预览通过此路径显示 Word 和 PowerPoint;[浏览器 Excel 预览](../feature/2026-09-16-browser-excel-preview.zh.md)独立读取 XLSX。LibreOffice 导入前,kit 校验 OOXML 输入的有界 ZIP 成员和内容类型,以及二进制 Office 输入的 OLE 复合文件头。将文本改为 Office 后缀不能通过校验。kit 不提取二进制格式的字体表,因此这些格式不返回缺失字体诊断。kit 在调用方拥有的私有目录中独占创建新的 PDF。DSH 读取并校验完整输出后才删除临时文件。[服务的 Remote 方法](../../../../packages/document/office-to-pdf/README.zh.md)通过 [Workspace Files](2026-09-09-workspace-file-read-authority.zh.md)授权源文件访问,保留源路径和版本,并返回 PDF 字节。源文件读取上限与生成 PDF 上限相互独立。读取权限探测和延迟读取(包括超限失败后的复查)采用同一个源路径/版本快照,防止转换将字节发布到另一个源身份下。预览字节不会进入 Session 存储或持久缓存。
converter 复用首个转换 Worker 返回的字体元数据;原始字体缓冲区和解码后的字符覆盖范围仍只属于单次转换。Worker 读取字体时校验索引中的文件。已安装字体族的精确匹配优先于配置的替代字体,完整的字体族、样式、字重、斜体、宽度、字距、语言与码点请求保留各自的匹配结果。WASM 回调将包含完整字体集合的原始字体文件导入 MEMFS。原生引擎还保留各平台的字体发现能力。两条路径均不下载或安装字体;原生操作系统管理的字体内存不受显式导入预算约束。字体变化后,重新创建 converter 会刷新元数据。 converter 复用首个转换 Worker 返回的字体元数据;原始字体缓冲区和解码后的字符覆盖范围仍只属于单次转换。Worker 读取字体时校验索引中的文件。已安装字体族的精确匹配优先于配置的替代字体,完整的字体族、样式、字重、斜体、宽度、字距、语言与码点请求保留各自的匹配结果。WASM 回调将包含完整字体集合的原始字体文件导入 MEMFS。原生引擎还保留各平台的字体发现能力。两条路径均不下载或安装字体;原生操作系统管理的字体内存不受显式导入预算约束。字体变化后,重新创建 converter 会刷新元数据。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-16-browser-excel-preview.md
2026-09-16-browser-excel-preview.md: 914d772229ada1e5f84524460510779f7624d3bc
2026-09-16-browser-excel-preview.zh.md: 1022ea293a36dfc032c9152bbbbc5682b12f9a98
@@ -0,0 +1,31 @@
# Agent Note: Browser Excel preview with saved formula results
Status: implemented
English | [中文](2026-09-16-browser-excel-preview.zh.md)
## Problem
PDF conversion loses spreadsheet navigation and the relationship between a formula and its saved value. Spreadsheet preview also needlessly depends on the Host Office engine when the browser can read the workbook.
## Decision
The [document preview](../../../../packages/client/ui-sidebar-documentpreview/README.md#excel-preview) opens XLSX with FortuneSheet and a package-local ExcelJS adapter. The adapter accepts bytes and limits and returns worksheet data; it has no React state setters, timers, or component references. ExcelJS owns OOXML parsing, while the adapter owns the display mapping. The existing complete-file reader retains Session authorization, file versions, reload, and transient bytes.
Preview is read-only and retains formula text alongside saved results. It does not recalculate, so unsupported functions cannot replace a valid saved result with an error. A formula without a saved result remains blank with a workbook notice. The spreadsheet is neither an authoring tool nor a full Excel rendering engine.
A disposable browser Worker contains parser CPU work. A compressed-byte limit precedes parsing, a combined rectangular-area limit precedes FortuneSheet matrix allocation, and a timeout terminates stalled parsing. These limits do not claim a hard memory sandbox. Styles are scoped to the renderer; external hyperlinks are text only.
The pinned FortuneSheet React dependency carries a [small patch](../../../../patches/@[email protected]) that skips the input box's read-only layout state update unless formula-reference highlighting is enabled. The unconditional update otherwise causes a maximum-update-depth failure when switching worksheets under React 19. The browser scenario covers the failure through the bundled ESM entry; the CommonJS entry receives the same fix. Recheck this patch when upgrading FortuneSheet.
## Alternatives considered
**Adopt FortuneExcel's React-oriented conversion helper.** Its component setters and post-render sizing couple file conversion to one UI lifecycle. The local adapter can be tested without mounting a workbook and needs no second intermediate workbook model.
**Write an OOXML parser or create a new public package.** ExcelJS already owns the container and file semantics. One preview consumer does not justify another public API, Cordis service, or generic workbook abstraction; a second independent consumer can motivate extraction.
**Keep PDF as an Excel fallback or calculate every formula on import.** PDF retains neither spreadsheet interaction nor formula inspection, while recalculation can change saved results. Legacy XLS receives explicit XLSX guidance. The [Office engine decision](../architecture/2026-09-11-node-office-kit.md) remains applicable to Word/PowerPoint previews and independent conversion consumers; its conversion capability still supports spreadsheets.
## Consequences
The renderer trades complete Excel fidelity for browser-local navigation and reusable conversion code. The package README owns supported formats, resource defaults, and unsupported features. Real XLSX fixtures cover conversion, Worker tests cover cancellation and cleanup, and the shipped-profile browser scenario opens a workbook with Office conversion disabled and copies a saved XLOOKUP result under read-only settings.
@@ -0,0 +1,31 @@
# Agent Note:显示已保存公式结果的浏览器 Excel 预览
Status: implemented
[English](2026-09-16-browser-excel-preview.md) | 中文
## 问题
PDF 转换会丢失工作表导航以及公式与已保存值之间的关系。浏览器能够读取工作簿时,表格预览也没有必要依赖 Host Office 引擎。
## 决策
[文档预览](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md#excel-preview)使用 FortuneSheet 与包内 ExcelJS 适配层打开 XLSX。适配层接收字节和限制并返回工作表数据,不包含 React 状态设置器、计时器或组件引用。ExcelJS 负责 OOXML 解析,适配层负责显示映射。现有完整文件读取器保留 Session 授权、文件版本、重新载入和临时字节。
预览为只读,同时保留公式文本与已保存结果。它不重新计算,因此不受支持的函数不会用错误覆盖有效的已保存结果。没有已保存结果的公式保持为空,并显示工作簿提示。该表格既不是编写工具,也不是完整的 Excel 渲染引擎。
独立可释放的浏览器 Worker 承担解析 CPU 工作。解析前检查压缩字节限制,FortuneSheet 分配矩阵前检查矩形区域合计限制,并通过超时终止停滞的解析。这些限制不代表严格的内存沙箱。样式限定在渲染器区域内,外部超链接仅显示为文本。
固定版本的 FortuneSheet React 依赖带有一个[小补丁](../../../../patches/@[email protected]):只读输入框仅在启用公式引用高亮时才提交布局状态更新。无条件更新会在 React 19 下切换工作表时触发最大更新深度错误。浏览器场景通过打包后的 ESM 入口覆盖该故障;CommonJS 入口应用相同修复。升级 FortuneSheet 时需要重新检查此补丁。
## 考虑过的替代方案
**采用 FortuneExcel 面向 React 的转换辅助函数。** 其组件状态设置器和渲染后尺寸调整把文件转换耦合到一种 UI 生命周期。本地适配层无需挂载工作簿即可测试,也不需要第二种中间工作簿模型。
**自行编写 OOXML 解析器或创建新的公共包。** ExcelJS 已经负责容器和文件语义。单个预览使用方不足以支持另一套公共 API、Cordis 服务或通用工作簿抽象;出现第二个独立使用方时再考虑提取。
**保留 PDF 作为 Excel 兜底,或在导入时计算所有公式。** PDF 无法保留表格交互与公式查看,而重新计算可能改变已保存结果。旧版 XLS 显示明确的 XLSX 引导。[Office 引擎决策](../architecture/2026-09-11-node-office-kit.zh.md)仍适用于 Word/PowerPoint 预览与独立转换使用方;转换能力仍支持表格。
## 影响
渲染器用部分 Excel 保真度换取浏览器内导航和可复用的转换代码。包 README 负责说明支持的格式、资源默认值和不支持的功能。真实 XLSX fixture 覆盖转换,Worker 测试覆盖取消和清理,发布 profile 的浏览器场景在禁用 Office 转换后打开工作簿,并在只读设置下复制已保存的 XLOOKUP 结果。
+5
View File
@@ -38,6 +38,7 @@ External packages installed for runtime use or distributed inside the prebuilt b
| [`@browserbasehq/stagehand`](https://github.com/browserbase/stagehand) | MIT | | [`@browserbasehq/stagehand`](https://github.com/browserbase/stagehand) | MIT |
| [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) | MPL-2.0 | | [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) | MPL-2.0 |
| [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi) | MIT | | [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi) | MIT |
| [`@fortune-sheet/react`](https://github.com/ruilisi/fortune-sheet) | MIT |
| [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT | | [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
| [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT | | [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT |
| [`@lexical/history`](https://github.com/facebook/lexical) | MIT | | [`@lexical/history`](https://github.com/facebook/lexical) | MIT |
@@ -128,6 +129,7 @@ pnpm applies local patches to the following packages at install time, so shipped
- `@earendil-works/[email protected]` — [`patches/@[email protected]`](patches/@[email protected]) - `@earendil-works/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
- `@electron/[email protected]` — [`patches/@[email protected]`](patches/@[email protected]) - `@electron/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
- `@yao-pkg/[email protected]` — [`patches/@[email protected]`](patches/@[email protected]) - `@yao-pkg/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
- `[email protected]` — [`patches/[email protected]`](patches/[email protected]) - `[email protected]` — [`patches/[email protected]`](patches/[email protected])
@@ -165,6 +167,7 @@ External packages **directly declared** for development, tests, types, or toolin
| [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT | | [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT |
| [`@electron/get`](https://github.com/electron/get) | MIT | | [`@electron/get`](https://github.com/electron/get) | MIT |
| [`@electron/notarize`](https://github.com/electron/notarize) | MIT | | [`@electron/notarize`](https://github.com/electron/notarize) | MIT |
| [`@fortune-sheet/core`](https://github.com/ruilisi/fortune-sheet) | MIT |
| [`@lexical/headless`](https://github.com/facebook/lexical) | MIT | | [`@lexical/headless`](https://github.com/facebook/lexical) | MIT |
| [`@modelcontextprotocol/node`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT | | [`@modelcontextprotocol/node`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
| [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT | | [`@modelcontextprotocol/server`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
@@ -206,8 +209,10 @@ External packages **directly declared** for development, tests, types, or toolin
| [`electron-builder`](https://github.com/electron-userland/electron-builder) | MIT | | [`electron-builder`](https://github.com/electron-userland/electron-builder) | MIT |
| [`esbuild`](https://github.com/evanw/esbuild) | MIT | | [`esbuild`](https://github.com/evanw/esbuild) | MIT |
| [`eslint-plugin-sonarjs`](https://github.com/SonarSource/SonarJS) | LGPL-3.0-only | | [`eslint-plugin-sonarjs`](https://github.com/SonarSource/SonarJS) | LGPL-3.0-only |
| [`exceljs`](https://github.com/exceljs/exceljs) | MIT |
| [`extract-zip`](https://github.com/maxogden/extract-zip) | BSD-2-Clause | | [`extract-zip`](https://github.com/maxogden/extract-zip) | BSD-2-Clause |
| [`fast-check`](https://github.com/dubzzz/fast-check) | MIT | | [`fast-check`](https://github.com/dubzzz/fast-check) | MIT |
| [`fast-xml-parser`](https://github.com/NaturalIntelligence/fast-xml-parser) | MIT |
| [`http-server`](https://github.com/http-party/http-server) | MIT | | [`http-server`](https://github.com/http-party/http-server) | MIT |
| [`istanbul-lib-report`](https://github.com/istanbuljs/istanbuljs) | BSD-3-Clause | | [`istanbul-lib-report`](https://github.com/istanbuljs/istanbuljs) | BSD-3-Clause |
| [`jscpd`](https://github.com/kucherenko/jscpd) | MIT | | [`jscpd`](https://github.com/kucherenko/jscpd) | MIT |
+33 -16
View File
@@ -9,6 +9,7 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest' import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest'
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment' import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
import { realOfficeBytes } from './office-fixture.ts' import { realOfficeBytes } from './office-fixture.ts'
import { excelFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/excel-fixture.ts'
import { pdfFixture, selectionPdfFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/pdf-fixture.ts' import { pdfFixture, selectionPdfFixture } from '../../../packages/client/ui-sidebar-documentpreview/tests/pdf-fixture.ts'
import { assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts' import { assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
@@ -243,6 +244,7 @@ else process.exit(1);
writeFile(join(cwd, 'user-unit.pdf'), pdfFixture(2)), writeFile(join(cwd, 'user-unit.pdf'), pdfFixture(2)),
...[90, 180, 270].map(rotation => writeFile(join(cwd, `rotated-${rotation}.pdf`), pdfFixture(4, rotation))), ...[90, 180, 270].map(rotation => writeFile(join(cwd, `rotated-${rotation}.pdf`), pdfFixture(4, rotation))),
writeFile(join(cwd, 'selection.pdf'), selectionPdfFixture()), writeFile(join(cwd, 'selection.pdf'), selectionPdfFixture()),
writeFile(join(cwd, 'budget.xlsx'), await excelFixture()),
...['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].map(extension => writeFile(join(cwd, `unavailable.${extension}`), Buffer.from('PK\u0003\u0004OFFICE_BINARY_PREVIEW'))), ...['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].map(extension => writeFile(join(cwd, `unavailable.${extension}`), Buffer.from('PK\u0003\u0004OFFICE_BINARY_PREVIEW'))),
writeFile(join(cwd, 'clip.mp4'), Buffer.from([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70])), writeFile(join(cwd, 'clip.mp4'), Buffer.from([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70])),
]) ])
@@ -751,16 +753,37 @@ else process.exit(1);
'- Plain-text option and viewer picker: hidden', '- Plain-text option and viewer picker: hidden',
].join('\n')) ].join('\n'))
const spreadsheetStates: string[] = [] await openFile('budget.xlsx')
for (const extension of ['xls', 'xlsx']) { const excel = preview.locator('[data-excel-preview]')
await openFile(`unavailable.${extension}`) await excel.getByText('季度预算', { exact: true }).waitFor({ state: 'visible' })
const spreadsheet = column.locator('[data-textpreview-state="unsupported"]') await successShot(page, 'excel-budget')
await spreadsheet.waitFor({ timeout: 15_000 }) expect(await preview.locator('[data-pdf-preview]').count()).toBe(0)
expect(await spreadsheet.getByText('Preview is not available for this file type yet.', { exact: true }).count()).toBe(1) expect(await excel.locator('.fortune-toolbar').count()).toBe(0)
await spreadsheet.locator('[data-open-path-unpreviewable]').waitFor({ timeout: 15_000 }) await excel.getByText('公式与格式', { exact: true }).click()
spreadsheetStates.push(`${extension.toUpperCase()}: unsupported / Open in default app`) await excel.locator('.fortune-sheet-overlay').click({ position: { x: 70, y: 30 } })
} await expect.poll(() => excel.locator('.fortune-fx-input').innerText()).toBe('=_xlfn.XLOOKUP(1,{1},{42})')
sections.push(['## Spreadsheet preview', '', `- ${spreadsheetStates.join('\n- ')}`].join('\n')) expect(await excel.locator('.fortune-fx-input').getAttribute('contenteditable')).toBe('false')
await page.keyboard.press('ControlOrMeta+C')
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe('42\n')
await page.keyboard.type('999')
await page.keyboard.press('ControlOrMeta+C')
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe('42\n')
await successShot(page, 'excel-cached-formula')
await openFile('unavailable.xls')
const legacyGuidance = 'Save this legacy Excel file as .xlsx to preview it.'
await preview.getByText(legacyGuidance, { exact: true }).waitFor()
await openFile('unavailable.xlsx')
const invalidExcel = 'This Excel file could not be opened. It may be damaged or password protected.'
await preview.getByText(invalidExcel, { exact: true }).waitFor()
sections.push([
'## Browser Excel preview', '',
'- Opens without the Office conversion service',
'- Sheets: 季度预算 | 公式与格式; hidden worksheet omitted',
'- Cached XLOOKUP result copied: 42; typing leaves it unchanged',
'- Formula bar is read-only; PDF body and editing toolbar absent',
`- Legacy guidance: ${legacyGuidance}`,
`- Invalid XLSX: ${invalidExcel}`,
].join('\n'))
await openFile('notes.unknown') await openFile('notes.unknown')
const plainLines = preview.locator('[data-textpreview-line]') const plainLines = preview.locator('[data-textpreview-line]')
@@ -958,12 +981,6 @@ describe.skipIf(MODE === 'record')('web e2e: Host Office preview', () => {
await successShot(page, `office-${extension}`) await successShot(page, `office-${extension}`)
} }
expect(convert).toHaveBeenCalledTimes(4) expect(convert).toHaveBeenCalledTimes(4)
for (const extension of ['xls', 'xlsx']) {
await openPreviewFile(column, filesTab, preview, `chinese.${extension}`)
await preview.getByText('Preview is not available for this file type yet.', { exact: true }).waitFor({ timeout: 15_000 })
expect(await preview.getByRole('img', { name: 'PDF page 1', exact: true }).count()).toBe(0)
}
expect(convert).toHaveBeenCalledTimes(4)
await openPreviewFile(column, filesTab, preview, 'chinese.docx') await openPreviewFile(column, filesTab, preview, 'chinese.docx')
await preview.getByRole('img', { name: 'PDF page 1', exact: true }).waitFor({ state: 'visible' }) await preview.getByRole('img', { name: 'PDF page 1', exact: true }).waitFor({ state: 'visible' })
await preview.getByRole('button', { name: 'Read the file again', exact: true }).click() await preview.getByRole('button', { name: 'Read the file again', exact: true }).click()
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md # pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 1fac327a8a50a09cf694f6fc31a0af1f031f596f config-catalog.md: 020bc3327493ab1c82b5cbb4b0742168ee8fb393
config-catalog.zh.md: fbc043ab62d57d1b7f754e28d3469cc5ebd4fb49 config-catalog.zh.md: e25e7331601301972afa29dfc893f0e6f5dafe6d
+9
View File
@@ -502,6 +502,15 @@ export interface Config {
/** Maximum readers including source and renderer metadata lookups. */ /** Maximum readers including source and renderer metadata lookups. */
maxReaders: number maxReaders: number
} }
/** Browser Excel parser and dense spreadsheet allocation limits. */
excel: {
/** Maximum compressed workbook bytes. */
maxBytes: number
/** Maximum combined rectangular cell area across worksheets. */
maxCells: number
/** Maximum parser Worker lifetime in milliseconds. */
timeoutMs: number
}
} }
``` ```
+9
View File
@@ -506,6 +506,15 @@ export interface Config {
/** Maximum readers including source and renderer metadata lookups. */ /** Maximum readers including source and renderer metadata lookups. */
maxReaders: number maxReaders: number
} }
/** Browser Excel parser and dense spreadsheet allocation limits. */
excel: {
/** Maximum compressed workbook bytes. */
maxBytes: number
/** Maximum combined rectangular cell area across worksheets. */
maxCells: number
/** Maximum parser Worker lifetime in milliseconds. */
timeoutMs: number
}
} }
``` ```
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md
README.md: 79c7253887cd580bdcc725054c0bbf5b2877e90b README.md: b37a393b1a41d78198a5c2a7dfab786c42b3d770
README.zh.md: 77847e8ce7a9203a85fee360ec8a481e32e7e871 README.zh.md: c867d34c544434b2541bfd9ab1ae4fef45469a35
@@ -9,13 +9,14 @@ English | [中文](README.zh.md)
## Summary ## Summary
Preview files in the right Sidebar and choose among registered renderers. Markdown and code support paged text; PDF, HTML, and common images receive complete bytes; unknown extensions use plain text. Word and PowerPoint documents convert locally to PDF; Excel workbooks use the unsupported-preview state. The tab provides file status, renderer selection, wrap, and automatic or manual reload. Plugins can add local opening controls to the header and unsupported-preview empty state. Preview files in the right Sidebar and choose among registered renderers. Markdown and code support paged text; PDF, HTML, common images, and XLSX receive complete bytes; unknown extensions use plain text. Word and PowerPoint documents convert locally to PDF; Excel workbooks open in the browser. The tab provides file status, renderer selection, wrap, and automatic or manual reload. Plugins can add local opening controls to the header and unsupported-preview empty state.
## Table of Contents ## Table of Contents
- [What it registers](#what-it-registers) - [What it registers](#what-it-registers)
- [Addresses](#addresses) - [Addresses](#addresses)
- [How it reads](#how-it-reads) - [How it reads](#how-it-reads)
- [Excel preview](#excel-preview)
- [Office preview](#office-preview) - [Office preview](#office-preview)
- [Navigation](#navigation) - [Navigation](#navigation)
- [Model Experience](#model-experience) - [Model Experience](#model-experience)
@@ -62,10 +63,25 @@ Shared copy comes from `sidebarDocumentPreview`; each builtin renderer owns its
Initial reads, additional pages, and HTML/PDF/image preparation share the ongoing `StateDot` loader, which exposes its label to assistive technology and respects reduced-motion preferences; every wait before content exists centres the loader in the pane, so opening a file shows one indicator in one position until the body appears. Loaded pages stay visible while another page loads. The PDF body loads its package-local `client.pdf.js` chunk only when a PDF preview mounts; PDF.js, its Worker source, and embedded support data stay out of the startup `client.js`. PDF pages fill the pane's width edge to edge as one vertical sequence and render lazily near the viewport; an unrendered page holds its place as a quiet 3:4 placeholder block. PDF.js’s official TextLayerBuilder manages selection boundaries and normalized copying over an aligned text layer. Its companion styles keep blank line breaks unhighlighted; alignment accounts for PDF page units, page rotation, and viewport resizing, and page disposal cancels both layers. Image-only PDFs contain no selectable text. Code previews show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill; its banner is adjacent to a full-height inner scrollport, so both scrollbars begin below the copy control. Initial reads, additional pages, and HTML/PDF/image preparation share the ongoing `StateDot` loader, which exposes its label to assistive technology and respects reduced-motion preferences; every wait before content exists centres the loader in the pane, so opening a file shows one indicator in one position until the body appears. Loaded pages stay visible while another page loads. The PDF body loads its package-local `client.pdf.js` chunk only when a PDF preview mounts; PDF.js, its Worker source, and embedded support data stay out of the startup `client.js`. PDF pages fill the pane's width edge to edge as one vertical sequence and render lazily near the viewport; an unrendered page holds its place as a quiet 3:4 placeholder block. PDF.js’s official TextLayerBuilder manages selection boundaries and normalized copying over an aligned text layer. Its companion styles keep blank line breaks unhighlighted; alignment accounts for PDF page units, page rotation, and viewport resizing, and page disposal cancels both layers. Image-only PDFs contain no selectable text. Code previews show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill; its banner is adjacent to a full-height inner scrollport, so both scrollbars begin below the copy control.
<a id="excel-preview"></a>
## Excel preview
Open `.xlsx` directly in the browser with worksheet tabs, cell selection, copying, and a read-only formula bar. The viewer retains fonts, solid fills, borders, number formats, rich text, merged cells, row and column sizes, hidden rows/columns/sheets, and frozen headings. It displays saved formula results without recalculating; missing results receive a notice. Legacy `.xls` files require saving as `.xlsx`. Excel preview does not call the Office conversion service.
Configure `excel` on the same `ui-sidebar-documentpreview` entry. These limits complement the Host's complete-file read limit; they do not cap browser process memory or decompression allocations.
| Field | Default | Meaning |
|---|---|---|
| `excel.maxBytes` | `16777216` (16 MiB) | Maximum compressed XLSX bytes |
| `excel.maxCells` | `250000` | Maximum combined rectangular worksheet area, including empty cells |
| `excel.timeoutMs` | `15000` | Maximum parser Worker lifetime in milliseconds |
The lazy Excel chunk bundles MIT-licensed FortuneSheet and ExcelJS. A package-local, React-independent adapter maps ExcelJS data to FortuneSheet cells. Each parse owns a disposable Worker and transfers a copy of retained file bytes; replacement, unmount, failure, and timeout terminate that Worker. The stylesheet is scoped to the Excel preview. Charts, drawings/images, pivot tables, conditional formatting, editing, recalculation, and export are unsupported; font availability, Excel column-width approximation, and theme-tint approximation can affect fidelity. Hyperlinks display as text without loading their targets.
<a id="office-preview"></a> <a id="office-preview"></a>
## Office preview ## Office preview
Open `.doc`, `.docx`, `.ppt`, and `.pptx` as PDF previews with the same loading state, controls, cancellation, and selectable text as PDF files. `.xls` and `.xlsx` do not register a Sidebar renderer: they show the unsupported-preview message without requesting conversion and offer the system default application when the Host reports a desktop. The [Host provider](../../document/office-to-pdf/README.md) retains its spreadsheet conversion API for other consumers. Invalid supported files, conversion failures, and timeouts receive localized messages. Missing Host services show configuration guidance. Open `.doc`, `.docx`, `.ppt`, and `.pptx` as PDF previews with the same loading state, controls, cancellation, and selectable text as PDF files. The [Host provider](../../document/office-to-pdf/README.md) performs local conversion and retains its spreadsheet conversion API for other consumers. Invalid supported files, conversion failures, and timeouts receive localized messages. Missing Host services show configuration guidance.
The [Web bundle](../../bundle/web-app/README.md) mounts this package as `ui-sidebar-documentpreview`. Configure its transient Office cache through that entry's `office` settings; the [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-client-ui-sidebar-documentpreview) defines accepted values. Settings are embedded in each served page; reload the browser page after changing YAML. The [Web bundle](../../bundle/web-app/README.md) mounts this package as `ui-sidebar-documentpreview`. Configure its transient Office cache through that entry's `office` settings; the [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-client-ui-sidebar-documentpreview) defines accepted values. Settings are embedded in each served page; reload the browser page after changing YAML.
@@ -108,7 +124,7 @@ No direct effect; what the user reads here never enters a model request.
<a id="known-limitations-and-deferred-work"></a> <a id="known-limitations-and-deferred-work"></a>
- **Preview, not editing.** The viewers provide no file editing or shared search interface; a directory address fails with `not-regular-file`. Unknown extensions use the plain-text reader and remain subject to its UTF-8/NUL checks. - **Preview, not editing.** The viewers provide no file editing or shared search interface; a directory address fails with `not-regular-file`. Unknown extensions use the plain-text reader and remain subject to its UTF-8/NUL checks.
- **Office conversion limits.** The preview does not launch native Office editors or download an engine. Excel workbooks are not previewed and instead offer the system default application on desktop Hosts. Binary `.doc` and `.ppt` files return no missing-font diagnostics. Conversion fidelity and resource limits belong to the [LibreOffice provider](../../document/office-to-pdf/README.md). - **Office conversion limits.** The preview does not launch native Office editors or download an engine. Binary `.doc` and `.ppt` files return no missing-font diagnostics. Conversion fidelity and resource limits belong to the [LibreOffice provider](../../document/office-to-pdf/README.md).
- **Sequential text and bounded complete files.** Deep source lines require the preceding pages; PDF, HTML, and images require a complete result within the Host's `maxFileBytes` cap. - **Sequential text and bounded complete files.** Deep source lines require the preceding pages; PDF, HTML, and images require a complete result within the Host's `maxFileBytes` cap.
- **Byte-view scroll state is not restored.** PDF, HTML, and images can return to the top when their renderer remounts or reloads; images fit the pane's width and never scroll horizontally, and HTML iframe scrolling belongs to its opaque browsing context. - **Byte-view scroll state is not restored.** PDF, HTML, and images can return to the top when their renderer remounts or reloads; images fit the pane's width and never scroll horizontally, and HTML iframe scrolling belongs to its opaque browsing context.
- **Finite local HTML dependencies.** Only direct classic `.js` and stylesheet `.css` references are packed. Browser-resolved resources retain browser origin and network restrictions; no runtime file-read bridge is exposed to the iframe. - **Finite local HTML dependencies.** Only direct classic `.js` and stylesheet `.css` references are packed. Browser-resolved resources retain browser origin and network restrictions; no runtime file-read bridge is exposed to the iframe.
@@ -9,13 +9,14 @@ kind: "package-reference"
## 概述 ## 概述
在右侧 Sidebar 预览文件,并切换已注册的渲染器。Markdown 和代码支持分页文本;PDF、HTML 和常见图片接收完整字节;未知扩展名使用纯文本。Word 与 PowerPoint 文档在本地转换为 PDF;Excel 工作簿使用不支持预览的空态。tab 提供文件状态、渲染器选择、换行以及自动或手动重新载入。插件可在头部和不支持预览的空态中添加本地打开控件。 在右侧 Sidebar 预览文件,并切换已注册的渲染器。Markdown 和代码支持分页文本;PDF、HTML、常见图片和 XLSX 接收完整字节;未知扩展名使用纯文本。Word 与 PowerPoint 文档在本地转换为 PDF;Excel 工作簿在浏览器内打开。tab 提供文件状态、渲染器选择、换行以及自动或手动重新载入。插件可在头部和不支持预览的空态中添加本地打开控件。
## 目录 ## 目录
- [注册了什么](#what-it-registers) - [注册了什么](#what-it-registers)
- [地址](#addresses) - [地址](#addresses)
- [怎么读](#how-it-reads) - [怎么读](#how-it-reads)
- [Excel 预览](#excel-preview)
- [Office 预览](#office-preview) - [Office 预览](#office-preview)
- [导航](#navigation) - [导航](#navigation)
- [模型体验](#model-experience) - [模型体验](#model-experience)
@@ -62,10 +63,25 @@ PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `<img>` 静态
首次读取、追加页及 HTML/PDF/图片准备共用 ongoing `StateDot` loading,其标签暴露给辅助技术,并遵循减少动态效果偏好;内容出现前的每个等待都把 loading 居中在面板中,打开文件到正文出现始终是同一位置的一个标记。下一页加载期间保留已显示的内容。PDF 正文仅在 PDF 预览挂载时加载包内 `client.pdf.js` chunk;PDF.js、Worker 源码和内嵌支持数据不会进入启动 `client.js`。PDF 页面贴边占满面板宽度,组成一个纵向连续序列并在接近视口时惰性渲染;未渲染的页以安静的 3:4 占位块保持位置。PDF.js 官方 TextLayerBuilder 在与画面重合的文字层上管理选区边界和复制文本规范化。配套样式不高亮空白换行;对齐同时考虑 PDF 页面单位、页面旋转与视口宽度变化,页面释放时取消两层渲染。纯图片 PDF 不包含可选取的文字。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;复制条与占满剩余高度的内部滚动区相邻,因此横纵滚动条都从复制控件下方开始。 首次读取、追加页及 HTML/PDF/图片准备共用 ongoing `StateDot` loading,其标签暴露给辅助技术,并遵循减少动态效果偏好;内容出现前的每个等待都把 loading 居中在面板中,打开文件到正文出现始终是同一位置的一个标记。下一页加载期间保留已显示的内容。PDF 正文仅在 PDF 预览挂载时加载包内 `client.pdf.js` chunk;PDF.js、Worker 源码和内嵌支持数据不会进入启动 `client.js`。PDF 页面贴边占满面板宽度,组成一个纵向连续序列并在接近视口时惰性渲染;未渲染的页以安静的 3:4 占位块保持位置。PDF.js 官方 TextLayerBuilder 在与画面重合的文字层上管理选区边界和复制文本规范化。配套样式不高亮空白换行;对齐同时考虑 PDF 页面单位、页面旋转与视口宽度变化,页面释放时取消两层渲染。纯图片 PDF 不包含可选取的文字。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;复制条与占满剩余高度的内部滚动区相邻,因此横纵滚动条都从复制控件下方开始。
<a id="excel-preview"></a>
## Excel 预览
直接在浏览器中打开 `.xlsx`,支持工作表标签、单元格选择、复制和只读公式栏。查看器保留字体、纯色填充、边框、数字格式、富文本、合并单元格、行列尺寸、隐藏行列及工作表,以及冻结标题。它显示已保存的公式结果而不重新计算;缺少结果时显示提示。旧版 `.xls` 文件需要另存为 `.xlsx`。Excel 预览不调用 Office 转换服务。
在同一个 `ui-sidebar-documentpreview` 条目上配置 `excel`。这些限制补充 Host 的完整文件读取限制,但不限制浏览器进程内存或解压分配量。
| 字段 | 默认值 | 含义 |
|---|---|---|
| `excel.maxBytes` | `16777216`(16 MiB) | 最大压缩 XLSX 字节数 |
| `excel.maxCells` | `250000` | 所有工作表矩形区域的最大合计单元格数,包含空单元格 |
| `excel.timeoutMs` | `15000` | 解析 Worker 的最长存活时间,单位毫秒 |
惰性 Excel chunk 打包 MIT 许可的 FortuneSheet 和 ExcelJS。包内独立于 React 的适配层将 ExcelJS 数据映射为 FortuneSheet 单元格。每次解析拥有一个独立可释放的 Worker,并传输所保留文件字节的副本;内容替换、卸载、失败或超时都会终止该 Worker。样式表仅作用于 Excel 预览区域。暂不支持图表、绘图/图片、数据透视表、条件格式、编辑、重新计算和导出;字体可用性、Excel 列宽近似和主题色明暗近似会影响保真度。超链接显示为文本,不加载目标地址。
<a id="office-preview"></a> <a id="office-preview"></a>
## Office 预览 ## Office 预览
将 `.doc`、`.docx`、`.ppt` 和 `.pptx` 打开为 PDF 预览,使用与 PDF 文件相同的加载状态、控件、取消和文本选择能力。`.xls` 与 `.xlsx` 不注册 Sidebar 渲染器:它们不请求转换,直接显示不支持预览的提示;Host 报告存在桌面时,还提供用系统默认应用打开的控件。[Host 提供方](../../document/office-to-pdf/README.zh.md)仍为其他消费者保留电子表格转换 API。受支持的无效文件、转换失败和超时会显示本地化消息。缺少 Host 服务时显示配置引导。 将 `.doc`、`.docx`、`.ppt` 和 `.pptx` 打开为 PDF 预览,使用与 PDF 文件相同的加载状态、控件、取消和文本选择能力。[Host 提供方](../../document/office-to-pdf/README.zh.md)负责本地转换,并为其他消费者保留电子表格转换 API。受支持的无效文件、转换失败和超时会显示本地化消息。缺少 Host 服务时显示配置引导。
[Web bundle](../../bundle/web-app/README.zh.md) 以 `ui-sidebar-documentpreview` 挂载本包。通过该条目的 `office` 设置配置临时 Office 缓存;[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-client-ui-sidebar-documentpreview)定义可接受的值。设置注入到每个页面;修改 YAML 后重新加载浏览器页面。 [Web bundle](../../bundle/web-app/README.zh.md) 以 `ui-sidebar-documentpreview` 挂载本包。通过该条目的 `office` 设置配置临时 Office 缓存;[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-client-ui-sidebar-documentpreview)定义可接受的值。设置注入到每个页面;修改 YAML 后重新加载浏览器页面。
@@ -108,7 +124,7 @@ Office 注册、加载、缓存和字体提示位于 `src/client/office/`。Offi
<a id="known-limitations-and-deferred-work"></a> <a id="known-limitations-and-deferred-work"></a>
- **预览而非编辑。** 查看器不提供文件编辑或共享搜索接口;目录地址以 `not-regular-file` 失败。未知扩展名使用纯文本读取,仍受其 UTF-8/NUL 检查限制。 - **预览而非编辑。** 查看器不提供文件编辑或共享搜索接口;目录地址以 `not-regular-file` 失败。未知扩展名使用纯文本读取,仍受其 UTF-8/NUL 检查限制。
- **Office 转换限制。** 预览不启动原生 Office 编辑器,也不下载引擎。Excel 工作簿不提供预览,在桌面 Host 上改为提供用系统默认应用打开的控件。二进制 `.doc` 和 `.ppt` 文件不返回缺失字体诊断。转换保真度与资源限制由 [LibreOffice 提供方](../../document/office-to-pdf/README.zh.md)负责。 - **Office 转换限制。** 预览不启动原生 Office 编辑器,也不下载引擎。二进制 `.doc` 和 `.ppt` 文件不返回缺失字体诊断。转换保真度与资源限制由 [LibreOffice 提供方](../../document/office-to-pdf/README.zh.md)负责。
- **文本顺序分页,完整文件受限。** 定位到较深处的源码行需要先加载此前各页;PDF、HTML 和图片必须取得 Host `maxFileBytes` 上限内的完整结果。 - **文本顺序分页,完整文件受限。** 定位到较深处的源码行需要先加载此前各页;PDF、HTML 和图片必须取得 Host `maxFileBytes` 上限内的完整结果。
- **字节视图不恢复滚动位置。** PDF、HTML 与图片的渲染器重新挂载或重新载入时可能回到顶部;图片适配面板宽度、不产生横向滚动,HTML iframe 的滚动属于其不透明浏览上下文。 - **字节视图不恢复滚动位置。** PDF、HTML 与图片的渲染器重新挂载或重新载入时可能回到顶部;图片适配面板宽度、不产生横向滚动,HTML iframe 的滚动属于其不透明浏览上下文。
- **本地 HTML 依赖集合有限。** 只打包直接引用的经典 `.js` 脚本和 `.css` 样式表。浏览器解析的资源仍受浏览器源与网络规则限制;iframe 不获得运行时文件读取桥接。 - **本地 HTML 依赖集合有限。** 只打包直接引用的经典 `.js` 脚本和 `.css` 样式表。浏览器解析的资源仍受浏览器源与网络规则限制;iframe 不获得运行时文件读取桥接。
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Suzhou Ruilisi Technology Co., Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -69,11 +69,16 @@
"@deepseek-ai/dsh-office-to-pdf": "workspace:^", "@deepseek-ai/dsh-office-to-pdf": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-util-workspace-path": "workspace:^", "@deepseek-ai/dsh-util-workspace-path": "workspace:^",
"@fortune-sheet/core": "1.0.4",
"@fortune-sheet/react": "1.0.4",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@types/react": "~18.3.1", "@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0", "@types/react-dom": "~18.3.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"dompurify": "^3.4.11", "dompurify": "^3.4.11",
"exceljs": "4.4.0",
"fast-xml-parser": "5.11.1",
"fflate": "^0.8.3",
"pdfjs-dist": "6.3.289", "pdfjs-dist": "6.3.289",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
@@ -0,0 +1,48 @@
.body {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
white-space: normal;
font-family: var(--dsw-font, sans-serif);
}
.workbook {
flex: 1;
min-height: 0;
min-width: 0;
color-scheme: light;
}
.notice {
flex: none;
margin: 0;
padding: 6px 12px;
font-size: 12px;
color: var(--dsw-alias-label-secondary);
border-bottom: 0.5px solid var(--dsw-alias-border-l3);
}
.status {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
height: 100%;
padding: 16px;
color: var(--dsw-alias-label-secondary);
}
.loadingIcon {
display: flex;
animation: turn 0.8s linear infinite;
}
@keyframes turn {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.loadingIcon { animation: none; }
}
@@ -0,0 +1,25 @@
/** Load the spreadsheet renderer only when a supported workbook is opened. */
import { lazy, Suspense, type ReactNode } from 'react'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { DocumentPreviewProps } from '../document/contract.ts'
import type { ExcelLimits } from './convert.ts'
import { hostFileOf } from '../rpc.ts'
import { LoadingIndicator } from '../LoadingIndicator.tsx'
import css from '../TextPreview.module.css'
/** Document input plus parser limits and localized copy. */
export type ExcelBodyProps = DocumentPreviewProps & PropsLocale<'sidebarExcel'> & { readonly limits: ExcelLimits }
const LoadedExcelBody = lazy(async () => ({ default: (await import('./excel.tsx')).ExcelBody }))
/**
* Show legacy-format guidance or load the browser workbook renderer.
* @param props - Complete file bytes and standard document seats.
* @returns Localized loading state or Excel preview.
*/
export function LazyExcelBody(props: ExcelBodyProps): ReactNode {
if (/\.xls$/iu.test(hostFileOf(props.resourceAddress).path)) return <p className={css.status} role="alert">{props.t('legacy')}</p>
return <Suspense fallback={<LoadingIndicator className={css.status} label={props.t('loading')} />}>
<LoadedExcelBody {...props} />
</Suspense>
}
@@ -0,0 +1,11 @@
/** Build-owned parser Worker source embedded in the lazy Excel chunk. */
declare module '*.ts?raw' {
const source: string
export default source
}
/** FortuneSheet's stylesheet is scoped by the preview component. */
declare module '@fortune-sheet/react/dist/index.css?inline' {
const source: string
export default source
}
@@ -0,0 +1,209 @@
/** XLSX parsing and FortuneSheet conversion without React or file-service dependencies. */
import ExcelJS, { type Cell as ExcelCell, type Color, type Font, type BorderStyle } from 'exceljs'
import { update as formatNumber, type Cell, type CellStyle, type Sheet, type SheetConfig } from '@fortune-sheet/core'
import { XMLParser } from 'fast-xml-parser'
/** Resource limits applied before allocating FortuneSheet's dense cell matrices. */
export interface ExcelLimits {
/** Maximum compressed workbook bytes. */
maxBytes: number
/** Maximum combined rectangular cell area across worksheets. */
maxCells: number
/** Maximum time allowed for one parser Worker. */
timeoutMs: number
}
/** Preview data and a count of formulas without saved results. */
export interface ExcelPreview {
sheets: Sheet[]
missingResults: number
}
/** Parser failures with locale-independent categories. */
export class ExcelPreviewError extends Error {
/** @param code - User-actionable parser failure. */
constructor(readonly code: 'invalid' | 'tooLarge' | 'timeout', options?: ErrorOptions) {
super(code, options)
this.name = 'ExcelPreviewError'
}
}
const BORDER_STYLES: Record<BorderStyle, number> = {
thin: 1, hair: 2, dotted: 3, dashed: 4, dashDot: 5, dashDotDot: 6,
double: 7, medium: 8, mediumDashed: 9, mediumDashDot: 10,
mediumDashDotDot: 11, slantDashDot: 12, thick: 13,
}
const THEME_ORDER = ['lt1', 'dk1', 'lt2', 'dk2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6', 'hlink', 'folHlink']
/**
* Decode an XLSX into display cells, retaining formulas without recalculation.
* @param bytes - Complete borrowed workbook bytes.
* @param limits - File and matrix allocation limits.
* @returns Sheets ready for a read-only FortuneSheet workbook.
*/
export async function convertExcel(bytes: Uint8Array<ArrayBuffer>, limits: ExcelLimits): Promise<ExcelPreview> {
if (bytes.byteLength > limits.maxBytes) throw new ExcelPreviewError('tooLarge')
const workbook = new ExcelJS.Workbook()
try {
await workbook.xlsx.load(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength))
} catch (error) {
throw new ExcelPreviewError('invalid', { cause: error })
}
if (!workbook.worksheets.some(sheet => sheet.state === 'visible')) throw new ExcelPreviewError('invalid')
let area = 0
for (const sheet of workbook.worksheets) {
area += Math.max(1, sheet.rowCount) * Math.max(1, sheet.columnCount)
if (area > limits.maxCells) throw new ExcelPreviewError('tooLarge')
}
const colors = themeColors(workbook)
let missingResults = 0
let active = false
const sheets = workbook.worksheets.map((worksheet, order): Sheet => {
const config: Required<Pick<SheetConfig, 'merge' | 'rowlen' | 'columnlen' | 'rowhidden' | 'colhidden' | 'borderInfo'>> = {
merge: {}, rowlen: {}, columnlen: {}, rowhidden: {}, colhidden: {}, borderInfo: [],
}
const celldata: NonNullable<Sheet['celldata']> = []
for (const range of worksheet.model.merges) {
// ExcelJS normalizes merged ranges to two cell addresses.
const [start, end] = range.split(':') as [string, string]
const first = worksheet.getCell(start)
const last = worksheet.getCell(end)
const merge = {
r: Number(first.row) - 1, c: Number(first.col) - 1,
rs: Number(last.row) - Number(first.row) + 1, cs: Number(last.col) - Number(first.col) + 1,
}
config.merge[`${merge.r}_${merge.c}`] = merge
}
// ExcelJS's declarations omit absent parsed collections and style fields.
const columns = worksheet.columns as ExcelJS.Column[] | null
;(columns ?? []).forEach((column, index) => {
if (column.width !== undefined) config.columnlen[index] = columnPixels(column.width)
if (column.hidden) config.colhidden[index] = 0
})
worksheet.eachRow({ includeEmpty: true }, (row) => {
const r = row.number - 1
const height = row.height as number | undefined
if (height !== undefined) config.rowlen[r] = height * 96 / 72
if (row.hidden) config.rowhidden[r] = 0
row.eachCell({ includeEmpty: true }, (cell) => {
const c = Number(cell.col) - 1
const v = convertCell(cell, colors)
if (v.f !== undefined && v.v === undefined) missingResults += 1
if (cell.isMerged) {
v.mc = config.merge[`${r}_${c}`] ?? { r: Number(cell.master.row) - 1, c: Number(cell.master.col) - 1 }
if (cell.address !== cell.master.address) { delete v.v; delete v.m; delete v.f }
}
celldata.push({ r, c, v })
const border = cell.border as Partial<ExcelJS.Borders> | undefined
if (border !== undefined) {
const value: Record<string, unknown> = { row_index: r, col_index: c }
for (const [side, key] of [['top', 't'], ['bottom', 'b'], ['left', 'l'], ['right', 'r']] as const) {
const edge = border[side]
if (edge?.style !== undefined) value[key] = { style: BORDER_STYLES[edge.style], color: colorOf(edge.color, colors) ?? '#000000' }
}
config.borderInfo.push({ rangeType: 'cell', value })
}
})
})
const view = (worksheet.views as ExcelJS.WorksheetView[] | null)?.[0]
const visible = worksheet.state === 'visible'
const status = visible && !active ? 1 : 0
active ||= visible
const sheet: Sheet = {
id: String(worksheet.id), name: worksheet.name, order, status, hide: visible ? 0 : 1,
row: Math.max(1, worksheet.rowCount), column: Math.max(1, worksheet.columnCount),
config, celldata, showGridLines: view?.showGridLines !== false,
defaultRowHeight: worksheet.properties.defaultRowHeight * 96 / 72,
}
if (worksheet.properties.defaultColWidth !== undefined) sheet.defaultColWidth = columnPixels(worksheet.properties.defaultColWidth)
if (view?.state === 'frozen' && (view.xSplit || view.ySplit)) {
sheet.frozen = {
type: view.xSplit && view.ySplit ? 'rangeBoth' : view.ySplit ? 'rangeRow' : 'rangeColumn',
range: { row_focus: (view.ySplit || 0) - 1, column_focus: (view.xSplit || 0) - 1 },
}
}
return sheet
})
return { sheets, missingResults }
}
function convertCell(
cell: Omit<ExcelCell, 'font' | 'fill' | 'alignment'> & Partial<Pick<ExcelCell, 'font' | 'fill' | 'alignment'>>,
colors: readonly (string | undefined)[],
): Cell {
const font = cell.font
const alignment = cell.alignment
const result: Cell = fontStyle(font, colors)
const bg = cell.fill?.type === 'pattern' && cell.fill.pattern === 'solid' ? colorOf(cell.fill.fgColor, colors) : undefined
if (bg !== undefined) result.bg = bg
if (alignment !== undefined) {
if (alignment.horizontal !== undefined) result.ht = alignment.horizontal === 'center' ? 0 : alignment.horizontal === 'right' ? 2 : 1
result.vt = alignment.vertical === 'top' ? 1 : alignment.vertical === 'middle' ? 0 : 2
result.tb = alignment.wrapText ? '2' : '1'
if (alignment.textRotation === 'vertical') result.tr = '3'
else if (alignment.textRotation !== undefined) result.rt = alignment.textRotation
}
const value = cell.value
let display: ExcelJS.CellValue = value
if (value !== null && typeof value === 'object' && ('formula' in value || 'sharedFormula' in value)) {
result.f = `=${cell.formula}`
display = value.result ?? null
}
if (display instanceof Date) {
// FortuneSheet uses the 1900 date system; ExcelJS has already resolved the workbook epoch.
result.v = display.getTime() / 86_400_000 + 25569
} else if (display !== null && typeof display === 'object') {
if ('richText' in display) {
result.ct = { t: 'inlineStr', s: display.richText.map(run => ({ v: run.text, ...fontStyle(run.font, colors) })) }
result.v = display.richText.map(run => run.text).join('')
} else if ('text' in display) result.v = display.text
else result.v = (display as ExcelJS.CellErrorValue).error
} else if (display !== null && display !== undefined) result.v = display
if (result.ht === undefined) result.ht = typeof result.v === 'number' ? 2 : typeof result.v === 'boolean' ? 0 : 1
if (result.ct === undefined) result.ct = { fa: cell.numFmt || 'General', t: typeof result.v === 'number' ? 'n' : typeof result.v === 'boolean' ? 'b' : 's' }
const formatted = typeof result.v === 'number' ? formatNumber(cell.numFmt || 'General', result.v) as string | number : result.v
result.m = formatted === undefined ? '' : String(formatted)
return result
}
function fontStyle(font: Partial<Font> | undefined, colors: readonly (string | undefined)[]): CellStyle {
if (font === undefined) return {}
const result: CellStyle = { bl: font.bold ? 1 : 0, it: font.italic ? 1 : 0, cl: font.strike ? 1 : 0, un: font.underline ? 1 : 0 }
if (font.name !== undefined) result.ff = font.name
if (font.size !== undefined) result.fs = font.size
const fc = colorOf(font.color, colors)
if (fc !== undefined) result.fc = fc
return result
}
function columnPixels(width: number): number { return Math.floor(width < 1 ? width * 12 : width * 7 + 5) }
function colorOf(color: Partial<Color> | undefined, themes: readonly (string | undefined)[]): string | undefined {
if (color === undefined) return undefined
const rgb = color.argb?.slice(-6) ?? (color.theme === undefined ? undefined : themes[color.theme])
if (rgb === undefined || !/^[\da-f]{6}$/iu.test(rgb)) return undefined
// ExcelJS retains OOXML tint even though its Color declaration omits it.
const tint: unknown = Reflect.get(color, 'tint')
if (typeof tint !== 'number' || tint === 0) return `#${rgb}`
return `#${[0, 2, 4].map((offset) => {
const channel = Number.parseInt(rgb.slice(offset, offset + 2), 16)
return Math.round(Math.max(0, Math.min(255, tint < 0 ? channel * (1 + tint) : channel * (1 - tint) + 255 * tint))).toString(16).padStart(2, '0')
}).join('')}`
}
function themeColors(workbook: ExcelJS.Workbook): (string | undefined)[] {
const themes: unknown = workbook.model.themes
const xml = record(themes).theme1
if (typeof xml !== 'string') return []
const parsed: unknown = new XMLParser({ ignoreAttributes: false, removeNSPrefix: true, processEntities: false }).parse(xml)
const scheme = record(record(record(parsed).theme).themeElements).clrScheme
return THEME_ORDER.map((name) => {
const color = record(record(scheme)[name])
const value = record(color.srgbClr)['@_val'] ?? record(color.sysClr)['@_lastClr']
return typeof value === 'string' ? value : undefined
})
}
function record(value: unknown): Record<string, unknown> {
return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
}
@@ -0,0 +1,51 @@
/** Read-only spreadsheet surface backed by browser-parsed workbook data. */
import { useEffect, useState, type ReactNode } from 'react'
import { Workbook } from '@fortune-sheet/react'
import { Button, IconLoadingOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import fortuneCss from '@fortune-sheet/react/dist/index.css?inline'
import type { ExcelPreview } from './convert.ts'
import type { ExcelBodyProps } from './LazyExcelBody.tsx'
import { parseExcel } from './parse.ts'
import css from './ExcelBody.module.css'
type State = { data: Uint8Array<ArrayBuffer> } & ({ value: ExcelPreview } | { error: string })
const scopedStyles = `@scope ([data-excel-preview]) { ${fortuneCss} }`
/**
* Display stored spreadsheet values and formats without editing or recalculation.
* @param props - Complete workbook bytes, limits, and locale.
* @returns An isolated spreadsheet surface with cancellable loading.
*/
export function ExcelBody({ content, limits, t }: ExcelBodyProps): ReactNode {
const data = content.kind === 'bytes' ? content.data : undefined
const [state, setState] = useState<State>()
const [attempt, setAttempt] = useState(0)
useEffect(() => {
if (data === undefined) return
const controller = new AbortController()
setState(undefined)
void parseExcel(data, limits, controller.signal).then(
(value) => { if (!controller.signal.aborted) setState({ data, value }) },
(error: unknown) => { if (!controller.signal.aborted) setState({ data, error: error instanceof Error ? error.message : 'invalid' }) },
)
return () => { controller.abort() }
}, [data, limits, attempt])
if (data === undefined) return <p className={css.status} role="alert">{t('invalid')}</p>
if (state?.data !== data) return <span className={css.status} role="status" aria-label={t('loading')} data-document-loading>
<span className={css.loadingIcon} aria-hidden="true"><IconLoadingOutlineRegular /></span>
</span>
if ('error' in state) return <div className={css.status} role="alert">
<span>{t(state.error === 'tooLarge' ? 'tooLarge' : state.error === 'timeout' ? 'timeout' : 'invalid')}</span>
<Button size="sm" onClick={() => { setAttempt(value => value + 1) }}>{t('retry')}</Button>
</div>
return <section className={css.body} data-excel-preview aria-label={t('title')}>
<style>{scopedStyles}</style>
<p className={css.notice} title={t('limitations')}>{t('summary')}</p>
{state.value.missingResults > 0 && <p className={css.notice} role="status">{t('missingResults')}</p>}
<div className={css.workbook}>
<Workbook key={`${attempt}:${t('language')}`} data={state.value.sheets} lang={t('language')}
allowEdit={false} showToolbar={false} showFormulaBar showSheetTabs forceCalculation={false}
cellContextMenu={['copy']} headerContextMenu={[]} sheetTabContextMenu={[]} filterContextMenu={[]} />
</div>
</section>
}
@@ -0,0 +1,24 @@
/** Excel previews use ordinary authorized file bytes without Office conversion. */
import type { Context } from '@deepseek-ai/cordis'
import type {} from '../index.ts'
import type { Config } from '../../config.ts'
import { LazyExcelBody } from './LazyExcelBody.tsx'
import { en, zh } from './locales.ts'
/**
* Register the browser Excel viewer and its lifecycle-owned slot.
* @param ctx - Preview and locale registries.
* @param limits - Resolved parser limits.
*/
export function apply(ctx: Context, limits: Config['excel']): void {
const id = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/excel'
ctx.effect(() => ctx.locale.register('sidebarExcel', { zh, en }))
const t = ctx.locale.bind('sidebarExcel')
ctx.effect(() => ctx.documentPreviews.register({
id, extensions: ['xlsx', 'xls'], binaryExtensions: ['xlsx', 'xls'], priority: 'builtin',
title: () => t('title'), loading: 'bytes-complete', wrap: false,
}))
ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register({
name: 'sidebar.right.tab.document', key: id, locale: 'sidebarExcel', inject: () => ({ limits }),
}, LazyExcelBody)))
}
@@ -0,0 +1,33 @@
/** Locale-owned Excel preview controls and parser feedback. */
export const zh = {
title: 'Excel', language: 'zh', loading: '正在打开表格…',
invalid: '无法打开此 Excel 文件。文件可能已损坏或受密码保护。',
tooLarge: '此表格超过预览大小限制。', timeout: '打开表格超时。请缩小文件后重试。',
legacy: '请将此旧版 Excel 文件另存为 .xlsx 后预览。',
summary: '只读预览 · 显示已保存的公式结果',
limitations: '图表、图片、数据透视表和条件格式暂不显示。',
missingResults: '部分公式没有已保存的结果。请在 Excel 中重新计算并保存。',
retry: '重试',
} satisfies Record<string, string>
/** Excel preview dictionary keys. */
export type ExcelPreviewKey = keyof typeof zh
/** English Excel preview copy. */
export const en = {
title: 'Excel', language: 'en', loading: 'Opening spreadsheet…',
invalid: 'This Excel file could not be opened. It may be damaged or password protected.',
tooLarge: 'This workbook exceeds the preview size limit.', timeout: 'Opening this workbook timed out. Try a smaller file.',
legacy: 'Save this legacy Excel file as .xlsx to preview it.',
summary: 'Read-only preview · Saved formula results',
limitations: 'Charts, images, pivot tables, and conditional formatting are not displayed yet.',
missingResults: 'Some formulas have no saved result. Recalculate and save the file in Excel.',
retry: 'Retry',
} satisfies Record<ExcelPreviewKey, string>
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** Excel preview status and third-party locale selection. */
sidebarExcel: ExcelPreviewKey
}
}
@@ -0,0 +1,64 @@
/** Browser lifetime for the isolated XLSX parser. */
import workerSource from './worker.ts?raw'
import type { ExcelLimits, ExcelPreview } from './convert.ts'
/**
* Parse workbook bytes in a disposable Worker, copying the retained preview buffer.
* @param bytes - Borrowed complete file bytes.
* @param limits - File, matrix, and elapsed-time limits.
* @param signal - Document body's lifetime.
* @returns Parsed sheets; rejects with a locale key on parser failures.
*/
export function parseExcel(bytes: Uint8Array<ArrayBuffer>, limits: ExcelLimits, signal: AbortSignal): Promise<ExcelPreview> {
signal.throwIfAborted()
if (bytes.byteLength > limits.maxBytes) return Promise.reject(new Error('tooLarge'))
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(new Blob([workerSource], { type: 'text/javascript' }))
let worker: Worker
try {
worker = new Worker(url, { name: 'dsh-excel' })
} catch (error) {
URL.revokeObjectURL(url)
reject(new Error('invalid', { cause: error }))
return
}
const finish = (): void => {
clearTimeout(timer)
signal.removeEventListener('abort', abort)
worker.onmessage = null
worker.onerror = null
worker.onmessageerror = null
worker.terminate()
URL.revokeObjectURL(url)
}
const abort = (): void => { finish(); reject(new DOMException('Excel preview closed', 'AbortError')) }
const fail = (): void => { finish(); reject(new Error('invalid')) }
const timer = setTimeout(() => { finish(); reject(new Error('timeout')) }, limits.timeoutMs)
worker.onerror = fail
worker.onmessageerror = fail
worker.onmessage = (event: MessageEvent<unknown>) => {
const message = event.data
if (typeof message !== 'object' || message === null || !('ok' in message)) { fail(); return }
if (message.ok === true && 'value' in message && validPreview(message.value)) {
finish()
resolve(message.value)
} else if (message.ok === false && 'code' in message && ['invalid', 'tooLarge', 'timeout'].includes(String(message.code))) {
finish()
reject(new Error(String(message.code)))
} else fail()
}
signal.addEventListener('abort', abort, { once: true })
try {
const copy = bytes.slice()
worker.postMessage({ bytes: copy, limits }, [copy.buffer])
} catch (error) { finish(); reject(new Error('invalid', { cause: error })) }
})
}
function validPreview(value: unknown): value is ExcelPreview {
if (typeof value !== 'object' || value === null || !('sheets' in value) || !('missingResults' in value)) return false
return typeof value.missingResults === 'number' && Number.isSafeInteger(value.missingResults) && value.missingResults >= 0
&& Array.isArray(value.sheets) && value.sheets.length > 0
&& value.sheets.every((sheet: unknown) => typeof sheet === 'object' && sheet !== null && 'name' in sheet && typeof sheet.name === 'string'
&& 'celldata' in sheet && Array.isArray(sheet.celldata))
}
@@ -0,0 +1,9 @@
/** One workbook per disposable parser Worker; formulas and external links are never executed. */
import { convertExcel, ExcelPreviewError, type ExcelLimits } from './convert.ts'
globalThis.onmessage = (event: MessageEvent<{ bytes: Uint8Array<ArrayBuffer>; limits: ExcelLimits }>) => {
void convertExcel(event.data.bytes, event.data.limits).then(
(value) => { globalThis.postMessage({ ok: true, value }) },
(error: unknown) => { globalThis.postMessage({ ok: false, code: error instanceof ExcelPreviewError ? error.code : 'invalid' }) },
)
}
@@ -37,6 +37,7 @@ import { apply as registerImage } from './image/index.ts'
import { apply as registerPdf } from './pdf/index.ts' import { apply as registerPdf } from './pdf/index.ts'
import { apply as registerCode } from './code/index.ts' import { apply as registerCode } from './code/index.ts'
import { apply as registerOffice } from './office/index.ts' import { apply as registerOffice } from './office/index.ts'
import { apply as registerExcel } from './excel/index.ts'
import { Config } from '../config.ts' import { Config } from '../config.ts'
// Values stay package-private unless another package needs them; the plugin // Values stay package-private unless another package needs them; the plugin
@@ -125,4 +126,5 @@ export function apply(ctx: ClientContext): void {
registerPdf(ctx) registerPdf(ctx)
registerCode(ctx) registerCode(ctx)
registerOffice(ctx, config.office) registerOffice(ctx, config.office)
registerExcel(ctx, config.excel)
} }
@@ -14,14 +14,28 @@ export interface Config {
/** Maximum readers including source and renderer metadata lookups. */ /** Maximum readers including source and renderer metadata lookups. */
maxReaders: number maxReaders: number
} }
/** Browser Excel parser and dense spreadsheet allocation limits. */
excel: {
/** Maximum compressed workbook bytes. */
maxBytes: number
/** Maximum combined rectangular cell area across worksheets. */
maxCells: number
/** Maximum parser Worker lifetime in milliseconds. */
timeoutMs: number
}
} }
/** Deployment limits applied before Office preview registration. */ /** Deployment limits applied before Office preview registration. */
export const Config: z<{ office?: Partial<Config['office']> }, Config> = z.object({ export const Config: z<{ office?: Partial<Config['office']>; excel?: Partial<Config['excel']> }, Config> = z.object({
office: z.object({ office: z.object({
maxCachedEntries: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(8), maxCachedEntries: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(8),
maxCachedBytes: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(64 * 1024 * 1024), maxCachedBytes: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(64 * 1024 * 1024),
maxPending: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(8), maxPending: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(8),
maxReaders: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(32), maxReaders: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(32),
}), }),
excel: z.object({
maxBytes: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(16 * 1024 * 1024),
maxCells: z.natural().min(1).max(Number.MAX_SAFE_INTEGER).default(250_000),
timeoutMs: z.natural().min(1).max(2_147_483_647).default(15_000),
}),
}) })
@@ -28,6 +28,7 @@ import { HTML_BODY_ID } from '../src/client/html/index.ts'
import { ImageBody } from '../src/client/image/ImageBody.tsx' import { ImageBody } from '../src/client/image/ImageBody.tsx'
import { IMAGE_BODY_ID } from '../src/client/image/index.ts' import { IMAGE_BODY_ID } from '../src/client/image/index.ts'
import { LazyPdfBody } from '../src/client/pdf/LazyPdfBody.tsx' import { LazyPdfBody } from '../src/client/pdf/LazyPdfBody.tsx'
import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx'
import { PDF_BODY_ID } from '../src/client/pdf/index.ts' import { PDF_BODY_ID } from '../src/client/pdf/index.ts'
import { CodeBody } from '../src/client/code/CodeBody.tsx' import { CodeBody } from '../src/client/code/CodeBody.tsx'
import { en, zh } from '../src/client/locales.ts' import { en, zh } from '../src/client/locales.ts'
@@ -99,6 +100,7 @@ describe('ui-sidebar-documentpreview apply', () => {
['sidebar.right.tab.document.action', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', 'sidebarOffice', OfficeFontAction], ['sidebar.right.tab.document.action', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', 'sidebarOffice', OfficeFontAction],
['sidebar.right.tab.document', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', 'sidebarOffice', OfficeBody], ['sidebar.right.tab.document', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', 'sidebarOffice', OfficeBody],
['sidebar.right.tab.document.office.pdf', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', 'sidebarPdf', LazyPdfBody], ['sidebar.right.tab.document.office.pdf', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', 'sidebarPdf', LazyPdfBody],
['sidebar.right.tab.document', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/excel', 'sidebarExcel', LazyExcelBody],
]) ])
expect(registered[0]?.store).toBeDefined() expect(registered[0]?.store).toBeDefined()
expect(typeof registered[0]?.inject).toBe('function') expect(typeof registered[0]?.inject).toBe('function')
@@ -5,12 +5,14 @@ import { expect, it, onTestFinished } from 'vitest'
import { Config } from '../src/config.ts' import { Config } from '../src/config.ts'
import * as host from '../src/index.ts' import * as host from '../src/index.ts'
it('exposes only Client reuse limits on the Host entry', () => { it('exposes Client cache and spreadsheet limits on the Host entry', () => {
expect(host.Config).toBe(Config) expect(host.Config).toBe(Config)
expect(Config({})).toEqual({ office: { expect(Config({})).toEqual({ office: {
maxCachedEntries: 8, maxCachedBytes: 64 * 1024 * 1024, maxPending: 8, maxReaders: 32, maxCachedEntries: 8, maxCachedBytes: 64 * 1024 * 1024, maxPending: 8, maxReaders: 32,
} }) }, excel: { maxBytes: 16 * 1024 * 1024, maxCells: 250_000, timeoutMs: 15_000 } })
for (const key of ['maxCachedEntries', 'maxCachedBytes']) expect(() => Config({ office: { [key]: 0 } })).toThrow() for (const key of ['maxCachedEntries', 'maxCachedBytes']) expect(() => Config({ office: { [key]: 0 } })).toThrow()
for (const key of ['maxBytes', 'maxCells', 'timeoutMs']) expect(() => Config({ excel: { [key]: 0 } })).toThrow()
expect(() => Config({ excel: { timeoutMs: 2_147_483_648 } })).toThrow()
}) })
it('embeds YAML cache settings in browser pages and withdraws them on disposal', async () => { it('embeds YAML cache settings in browser pages and withdraws them on disposal', async () => {
@@ -0,0 +1,80 @@
// @vitest-environment jsdom
/** Excel preview lifecycle and read-only renderer settings. */
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { Config } from '../src/config.ts'
import { en } from '../src/client/excel/locales.ts'
import type { ExcelBodyProps } from '../src/client/excel/LazyExcelBody.tsx'
const mocked = vi.hoisted(() => ({ parse: vi.fn(), workbook: vi.fn((_props: unknown) => null) }))
vi.mock('../src/client/excel/parse.ts', () => ({ parseExcel: mocked.parse }))
vi.mock('@fortune-sheet/react', () => ({ Workbook: mocked.workbook }))
import { ExcelBody } from '../src/client/excel/excel.tsx'
import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx'
const props = { content: { kind: 'bytes', data: new Uint8Array([1]) }, limits: Config({}).excel, t: makeTranslate(en), resourceAddress: 'dsh-resource://file/session/s1/book.xlsx' } as ExcelBodyProps
const value = { sheets: [{ name: 'Budget', celldata: [] }], missingResults: 0 }
afterEach(() => { cleanup(); vi.resetAllMocks() })
it('shows loading then a workbook with editing and recalculation disabled', async () => {
mocked.parse.mockResolvedValue({ ...value, missingResults: 1 })
const view = render(<ExcelBody {...props} />)
expect(screen.getByRole('status', { name: en.loading })).toBeDefined()
await screen.findByText(en.summary)
expect(screen.getByText(en.missingResults)).toBeDefined()
expect(mocked.workbook.mock.calls[0]![0]).toMatchObject({ data: value.sheets, allowEdit: false, forceCalculation: false, showToolbar: false, showSheetTabs: true, lang: 'en', cellContextMenu: ['copy'] })
const signal = mocked.parse.mock.calls[0]![2] as AbortSignal
view.unmount()
expect(signal.aborted).toBe(true)
})
it('ignores a retired file result and cancels parsing when the bytes change', async () => {
let finish!: (value: unknown) => void
mocked.parse.mockReturnValueOnce(new Promise((resolve) => { finish = resolve })).mockResolvedValueOnce(value)
const view = render(<ExcelBody {...props} />)
const signal = mocked.parse.mock.calls[0]![2] as AbortSignal
view.rerender(<ExcelBody {...props} content={{ kind: 'bytes', data: new Uint8Array([2]) }} />)
await screen.findByText(en.summary)
expect(signal.aborted).toBe(true)
finish({ ...value, missingResults: 1 })
await waitFor(() => { expect(screen.queryByText(en.missingResults)).toBeNull() })
})
it.each(['invalid', 'tooLarge', 'timeout', 'unexpected'])('shows localized %s errors and retries explicitly', async (error) => {
mocked.parse.mockRejectedValueOnce(new Error(error)).mockResolvedValueOnce(value)
render(<ExcelBody {...props} />)
const key = error === 'tooLarge' || error === 'timeout' ? error : 'invalid'
await screen.findByText(en[key])
fireEvent.click(screen.getByRole('button', { name: en.retry }))
await screen.findByText(en.summary)
expect(mocked.parse).toHaveBeenCalledTimes(2)
})
it('rejects non-byte content and shows legacy XLS guidance without loading a parser', () => {
const view = render(<ExcelBody {...props} content={{ kind: 'text', text: '', pages: [], eof: true }} />)
expect(screen.getByRole('alert').textContent).toBe(en.invalid)
view.rerender(<LazyExcelBody {...props} resourceAddress="dsh-resource://file/session/s1/book.XLS" />)
expect(screen.getByRole('alert').textContent).toBe(en.legacy)
expect(mocked.parse).not.toHaveBeenCalled()
})
it('loads the spreadsheet chunk for an XLSX file', async () => {
mocked.parse.mockResolvedValue(value)
render(<LazyExcelBody {...props} />)
await screen.findByText(en.summary)
})
it('uses generic guidance for non-Error failures and ignores rejection after unmount', async () => {
mocked.parse.mockRejectedValueOnce('parser failure')
const view = render(<ExcelBody {...props} />)
await screen.findByText(en.invalid)
view.unmount()
let reject!: (error: Error) => void
mocked.parse.mockReturnValueOnce(new Promise((_resolve, fail) => { reject = fail }))
const pending = render(<ExcelBody {...props} />)
const signal = mocked.parse.mock.calls[1]![2] as AbortSignal
pending.unmount()
reject(new Error('closed'))
await waitFor(() => { expect(signal.aborted).toBe(true) })
})
@@ -0,0 +1,119 @@
/** XLSX formatting, cached formula results, and resource admission. */
import ExcelJS from 'exceljs'
import { strToU8, unzipSync, zipSync } from 'fflate'
import { describe, expect, it } from 'vitest'
import { convertExcel } from '../src/client/excel/convert.ts'
import { Config } from '../src/config.ts'
import { excelFixture } from './excel-fixture.ts'
const limits = Config({}).excel
describe('Excel conversion', () => {
it('keeps values, cached formulas, styles, merges, dimensions, visibility, and frozen headings', async () => {
const input = await excelFixture()
const { sheets, missingResults } = await convertExcel(input, limits)
expect(input.byteLength).toBeGreaterThan(0)
expect(sheets.map(sheet => sheet.name)).toEqual(['季度预算', '公式与格式', '隐藏页'])
const first = sheets[0]!
const cell = (r: number, c: number) => first.celldata!.find(value => value.r === r && value.c === c)!.v
expect(cell(0, 0)).toMatchObject({ v: 'DeepSeek · 项目预算', bl: 1, fs: 18, fc: '#FFFFFF', bg: '#3B5CCC', ht: 0, vt: 0, mc: { r: 0, c: 0, rs: 1, cs: 4 } })
expect(cell(0, 1)).toMatchObject({ mc: { r: 0, c: 0 } })
expect(cell(0, 1)).not.toHaveProperty('v')
expect(cell(2, 3)).toMatchObject({ f: '=C3/B3', v: 0.8, m: '80.0%', ct: { fa: '0.0%', t: 'n' } })
expect(cell(4, 1)).toMatchObject({ f: '=SUM(B3:B4)', v: 57000, m: '57,000.00' })
expect(first.config).toMatchObject({ rowlen: { 0: 48 }, columnlen: { 0: 173 }, merge: { '0_0': { rs: 1, cs: 4 } } })
expect(first.config!.borderInfo).toContainEqual({ rangeType: 'cell', value: { row_index: 1, col_index: 0, b: { style: 1, color: '#DCE2ED' } } })
expect(first.frozen).toEqual({ type: 'rangeBoth', range: { row_focus: 1, column_focus: 0 } })
const second = sheets[1]!
expect(second.celldata).toEqual(expect.arrayContaining([
expect.objectContaining({ r: 0, c: 0, v: expect.objectContaining({ f: '=_xlfn.XLOOKUP(1,{1},{42})', v: 42, m: '42' }) as unknown }),
expect.objectContaining({ r: 1, c: 0, v: expect.objectContaining({ f: '=SUM(1,2)', m: '' }) as unknown }),
expect.objectContaining({ r: 0, c: 1, v: expect.objectContaining({ m: '2026-09-16' }) as unknown }),
expect.objectContaining({ r: 0, c: 2, v: expect.objectContaining({ v: '富文本 示例', ct: { t: 'inlineStr', s: expect.any(Array) as unknown } }) as unknown }),
expect.objectContaining({ r: 0, c: 3, v: expect.objectContaining({ v: 'Link text' }) as unknown }),
expect.objectContaining({ r: 2, c: 0, v: expect.objectContaining({ f: '=SUM(1,2)', v: 3 }) as unknown }),
expect.objectContaining({ r: 3, c: 0, v: expect.objectContaining({ v: true }) as unknown }),
expect.objectContaining({ r: 4, c: 0, v: expect.objectContaining({ v: '#DIV/0!' }) as unknown }),
]))
expect(second.config).toMatchObject({ rowhidden: { 6: 0 }, colhidden: { 4: 0 }, rowlen: { 6: 40 * 96 / 72 } })
expect(sheets[2]).toMatchObject({ hide: 1, status: 0 })
expect(missingResults).toBe(1)
})
it('rejects damaged files, byte limits and sparse worksheets that would allocate a large matrix', async () => {
await expect(convertExcel(new Uint8Array([1, 2, 3]), limits)).rejects.toMatchObject({ code: 'invalid' })
const input = await excelFixture()
await expect(convertExcel(input, { ...limits, maxBytes: input.byteLength - 1 })).rejects.toMatchObject({ code: 'tooLarge' })
const workbook = new ExcelJS.Workbook()
workbook.addWorksheet('Sparse').getCell('Z1000').value = 1
await expect(convertExcel(new Uint8Array(await workbook.xlsx.writeBuffer()), { ...limits, maxCells: 25_000 })).rejects.toMatchObject({ code: 'tooLarge' })
})
it('opens an empty worksheet and rejects workbooks with no visible sheet', async () => {
const workbook = new ExcelJS.Workbook()
workbook.addWorksheet('Empty')
const result = await convertExcel(new Uint8Array(await workbook.xlsx.writeBuffer()), limits)
expect(result.sheets[0]).toMatchObject({ name: 'Empty', row: 1, column: 1, status: 1 })
workbook.getWorksheet('Empty')!.state = 'veryHidden'
await expect(convertExcel(new Uint8Array(await workbook.xlsx.writeBuffer()), limits)).rejects.toMatchObject({ code: 'invalid' })
})
it.each([false, true])('retains dates for the 1904 epoch flag %s', async (date1904) => {
const workbook = new ExcelJS.Workbook()
workbook.properties.date1904 = date1904
const cell = workbook.addWorksheet('Dates').getCell('A1')
cell.value = new Date('2024-02-29T12:00:00Z')
cell.numFmt = 'yyyy-mm-dd hh:mm'
const result = await convertExcel(new Uint8Array(await workbook.xlsx.writeBuffer()), limits)
expect(result.sheets[0]!.celldata![0]!.v).toMatchObject({ m: '2024-02-29 12:00' })
})
it('maps custom colors, tints, alignment, decoration, and default dimensions', async () => {
const workbook = new ExcelJS.Workbook()
const sheet = workbook.addWorksheet('Styles')
sheet.properties.defaultColWidth = 12
sheet.getColumn(1).width = 0.5
const light = { theme: 4, tint: 0.5 }
const dark = { theme: 4, tint: -0.5 }
sheet.getCell('A1').value = 'Light'
sheet.getCell('A1').font = { color: light, strike: true, underline: true }
sheet.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: dark }
sheet.getCell('A1').alignment = { horizontal: 'right', vertical: 'top', wrapText: true, textRotation: 'vertical' }
sheet.getCell('B1').value = 'Rotated'
sheet.getCell('B1').alignment = { horizontal: 'left', vertical: 'bottom', textRotation: 45 }
sheet.getCell('B1').border = { top: { style: 'double' } }
sheet.getCell('C1').value = 'Automatic color'
sheet.getCell('C1').font = { color: { indexed: 64 } as Partial<ExcelJS.Color> }
sheet.getCell('D1').value = 'Invalid color'
sheet.getCell('D1').font = { color: { argb: 'invalid' } }
sheet.getCell('E1').value = 'Untinted'
sheet.getCell('E1').font = { color: { argb: 'FFFF0000', tint: 0 } as Partial<ExcelJS.Color> }
sheet.getCell('F1').value = 42
sheet.getCell('F1').alignment = { vertical: 'middle' }
const result = await convertExcel(new Uint8Array(await workbook.xlsx.writeBuffer()), limits)
expect(result.sheets[0]).toMatchObject({ defaultColWidth: 89, config: { columnlen: { 0: 6 } } })
const cells = result.sheets[0]!.celldata!
expect(cells[0]!.v).toMatchObject({ fc: '#a7c0de', bg: '#28415f', cl: 1, un: 1, ht: 2, vt: 1, tb: '2', tr: '3' })
expect(cells[1]!.v).toMatchObject({ ht: 1, vt: 2, rt: 45 })
expect(cells[2]!.v).not.toHaveProperty('fc')
expect(cells[3]!.v).not.toHaveProperty('fc')
expect(cells[4]!.v).toMatchObject({ fc: '#FF0000' })
expect(cells[5]!.v).toMatchObject({ v: 42, ht: 2, vt: 0, ct: { t: 'n' } })
})
it.each([{ ySplit: 2 }, { xSplit: 1 }, {}])('maps one-axis or empty freeze settings %j', async (freeze) => {
const workbook = new ExcelJS.Workbook()
workbook.addWorksheet('Frozen', { views: [{ state: 'frozen', ...freeze, showGridLines: false }] }).getCell('A1').value = 1
const result = await convertExcel(new Uint8Array(await workbook.xlsx.writeBuffer()), limits)
expect(result.sheets[0]!.showGridLines).toBe(false)
expect(result.sheets[0]!.frozen?.type).toBe('ySplit' in freeze ? 'rangeRow' : 'xSplit' in freeze ? 'rangeColumn' : undefined)
})
it('accepts workbooks without a theme and ignores absent theme colors', async () => {
const files = unzipSync(await excelFixture())
delete files['xl/theme/theme1.xml']
expect((await convertExcel(new Uint8Array(zipSync(files)), limits)).sheets).toHaveLength(3)
files['xl/theme/theme1.xml'] = strToU8('<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:themeElements><a:clrScheme name="Custom"><a:lt1><a:sysClr val="window" lastClr="FEFEFE"/></a:lt1></a:clrScheme></a:themeElements></a:theme>')
expect((await convertExcel(new Uint8Array(zipSync(files)), limits)).sheets).toHaveLength(3)
})
})
@@ -0,0 +1,49 @@
/** Real XLSX fixture shared by adapter and shipped-profile browser tests. */
import ExcelJS from 'exceljs'
/**
* Build a styled, multi-sheet workbook with cached and unsupported formulas.
* @returns Complete XLSX file bytes.
*/
export async function excelFixture(): Promise<Uint8Array<ArrayBuffer>> {
const workbook = new ExcelJS.Workbook()
const sheet = workbook.addWorksheet('季度预算', { views: [{ state: 'frozen', xSplit: 1, ySplit: 2 }] })
sheet.columns = [{ width: 24 }, { width: 18 }, { width: 18 }, { width: 18 }]
sheet.mergeCells('A1:D1')
sheet.getCell('A1').value = 'DeepSeek · 项目预算'
sheet.getCell('A1').font = { name: 'Arial', size: 18, bold: true, color: { argb: 'FFFFFFFF' } }
sheet.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF3B5CCC' } }
sheet.getCell('A1').alignment = { horizontal: 'center', vertical: 'middle' }
sheet.getRow(1).height = 36
sheet.getRow(2).values = ['项目', '预算', '支出', '执行率']
sheet.getRow(3).values = ['设计', 12000, 9600, { formula: 'C3/B3', result: 0.8 }]
sheet.getRow(4).values = ['研发', 45000, 31500, { formula: 'C4/B4', result: 0.7 }]
sheet.getRow(5).values = ['合计', { formula: 'SUM(B3:B4)', result: 57000 }, { formula: 'SUM(C3:C4)', result: 41100 }, { formula: 'C5/B5', result: 41100 / 57000 }]
for (let row = 2; row <= 5; row += 1) {
sheet.getRow(row).height = 28
for (let column = 1; column <= 4; column += 1) {
const cell = sheet.getCell(row, column)
cell.border = { bottom: { style: 'thin', color: { argb: 'FFDCE2ED' } } }
if (row === 2 || row === 5) {
cell.font = { bold: true, color: { argb: 'FF24345C' } }
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFEDF1FC' } }
}
if (row > 2 && column > 1) cell.numFmt = column === 4 ? '0.0%' : '#,##0.00'
}
}
const details = workbook.addWorksheet('公式与格式')
details.getCell('A1').value = { formula: '_xlfn.XLOOKUP(1,{1},{42})', result: 42 }
details.getCell('A2').value = { formula: 'SUM(1,2)' }
details.getCell('B1').value = new Date('2026-09-16T00:00:00Z')
details.getCell('B1').numFmt = 'yyyy-mm-dd'
details.getCell('C1').value = { richText: [{ text: '富文本', font: { bold: true } }, { text: ' 示例', font: { italic: true } }] }
details.getCell('D1').value = { text: 'Link text', hyperlink: 'https://example.com' }
details.getCell('A3').value = { sharedFormula: 'A2', result: 3 }
details.getCell('A4').value = true
details.getCell('A5').value = { error: '#DIV/0!' }
details.getRow(7).height = 40
details.getRow(7).hidden = true
details.getColumn(5).hidden = true
workbook.addWorksheet('隐藏页', { state: 'hidden' }).getCell('A1').value = 'Hidden data'
return new Uint8Array(await workbook.xlsx.writeBuffer())
}
@@ -0,0 +1,99 @@
/** Parser Worker allocation, cancellation, timeout, and message failures. */
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { parseExcel } from '../src/client/excel/parse.ts'
import { Config } from '../src/config.ts'
const limits = Config({}).excel
const preview = { sheets: [{ name: 'Sheet', celldata: [] }], missingResults: 0 }
let instance: ParserWorker
const revokeUrl = vi.fn()
class ParserWorker {
onmessage: ((event: { data: unknown }) => void) | null = null
onerror: (() => void) | null = null
onmessageerror: (() => void) | null = null
postMessage = vi.fn<(message: { bytes: Uint8Array<ArrayBuffer> }, transfer: Transferable[]) => void>()
terminate = vi.fn()
}
beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('Worker', vi.fn(function () { instance = new ParserWorker(); return instance }))
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:excel-test')
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(revokeUrl)
})
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); vi.unstubAllGlobals(); revokeUrl.mockClear() })
it('transfers a private copy and releases the Worker and URL after parsing', async () => {
const input = new Uint8Array([1, 2, 3])
const result = parseExcel(input, limits, new AbortController().signal)
const [message, transfer] = instance.postMessage.mock.calls[0]!
expect(message.bytes).toEqual(input)
expect(message.bytes).not.toBe(input)
expect(transfer).toEqual([message.bytes.buffer])
instance.onmessage!({ data: { ok: true, value: preview } })
await expect(result).resolves.toEqual(preview)
expect(instance.terminate).toHaveBeenCalledOnce()
expect(revokeUrl).toHaveBeenCalledWith('blob:excel-test')
expect(vi.getTimerCount()).toBe(0)
})
it('does not allocate a Worker for aborted or oversized input', async () => {
const worker = vi.fn()
vi.stubGlobal('Worker', worker)
expect(() => parseExcel(new Uint8Array(), limits, AbortSignal.abort())).toThrow()
await expect(parseExcel(new Uint8Array(2), { ...limits, maxBytes: 1 }, new AbortController().signal)).rejects.toThrow('tooLarge')
expect(worker).not.toHaveBeenCalled()
})
it('cancels an active parser without retaining callbacks or timers', async () => {
const controller = new AbortController()
const result = parseExcel(new Uint8Array(), limits, controller.signal)
const rejection = expect(result).rejects.toThrow('closed')
controller.abort(new Error('closed'))
await rejection
expect(instance.terminate).toHaveBeenCalledOnce()
expect(instance.onmessage).toBeNull()
expect(vi.getTimerCount()).toBe(0)
})
it('terminates a parser that exceeds its time budget', async () => {
const result = parseExcel(new Uint8Array(), limits, new AbortController().signal)
const rejection = expect(result).rejects.toThrow('timeout')
await vi.advanceTimersByTimeAsync(limits.timeoutMs)
await rejection
expect(instance.terminate).toHaveBeenCalledOnce()
})
it.each([null, 7, {}, { ok: true, value: null }, { ok: true, value: { sheets: [], missingResults: 0 } },
{ ok: true, value: { sheets: [{}], missingResults: 0 } }, { ok: true, value: { ...preview, missingResults: -1 } },
{ ok: false, code: 'surprise' }, { ok: false, code: 'invalid' }])('rejects invalid Worker message %j', async (data) => {
const result = parseExcel(new Uint8Array(), limits, new AbortController().signal)
instance.onmessage!({ data })
await expect(result).rejects.toThrow('invalid')
expect(instance.terminate).toHaveBeenCalledOnce()
})
it.each(['onerror', 'onmessageerror'] as const)('releases a failed parser on %s', async (event) => {
const result = parseExcel(new Uint8Array(), limits, new AbortController().signal)
instance[event]!()
await expect(result).rejects.toThrow('invalid')
expect(instance.terminate).toHaveBeenCalledOnce()
})
it('revokes the URL when Worker construction fails', async () => {
vi.stubGlobal('Worker', vi.fn(function () { throw new Error('unavailable') }))
await expect(parseExcel(new Uint8Array(), limits, new AbortController().signal)).rejects.toThrow('invalid')
expect(revokeUrl).toHaveBeenCalledOnce()
})
it('terminates the Worker when posting the copied buffer fails', async () => {
vi.stubGlobal('Worker', vi.fn(function () {
instance = new ParserWorker()
instance.postMessage.mockImplementation(() => { throw new Error('transfer failed') })
return instance
}))
await expect(parseExcel(new Uint8Array(), limits, new AbortController().signal)).rejects.toThrow('invalid')
expect(instance.terminate).toHaveBeenCalledOnce()
expect(vi.getTimerCount()).toBe(0)
})
@@ -0,0 +1,40 @@
/** Excel registration remains independent of the Office conversion service. */
import { Context } from '@deepseek-ai/cordis'
import { expect, it, vi } from 'vitest'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { DocumentPreviewRegistry } from '../src/client/document/registry.ts'
import { Config } from '../src/config.ts'
import { apply } from '../src/client/excel/index.ts'
import { LazyExcelBody } from '../src/client/excel/LazyExcelBody.tsx'
import { en, zh } from '../src/client/excel/locales.ts'
it('registers complete binary reads and removes the slot and locale on disposal', async () => {
const ctx = new Context()
const registry = new DocumentPreviewRegistry()
const removeLocale = vi.fn()
const removeBody = vi.fn()
const registerLocale = vi.fn(() => removeLocale)
const register = vi.fn((_options: unknown, _component: unknown) => removeBody)
ctx.provide('documentPreviews', registry)
ctx.provide('locale', { register: registerLocale, bind: () => makeTranslate(en) } as never)
ctx.provide('slots', { inject: (_name: string, callback: () => () => void) => callback(), register } as never)
const limits = Config({}).excel
const fiber = ctx.plugin({ apply: (scope) => { apply(scope, limits) } })
try {
await fiber.await()
for (const path of ['budget.XLSX', 'legacy.xls']) {
const candidate = registry.candidates(path)[0]!
expect(candidate).toMatchObject({ loading: 'bytes-complete', wrap: false, binaryExtensions: ['xlsx', 'xls'] })
expect(candidate.title()).toBe('Excel')
expect(candidate.read).toBeUndefined()
expect(candidate.bodyId).toBeUndefined()
}
expect(registerLocale).toHaveBeenCalledWith('sidebarExcel', { zh, en })
expect(register).toHaveBeenCalledWith(expect.objectContaining({ locale: 'sidebarExcel', inject: expect.any(Function) as unknown }), LazyExcelBody)
const options = register.mock.calls[0]![0] as { inject: () => unknown }
expect(options.inject()).toEqual({ limits })
} finally { await fiber.dispose() }
expect(registry.getSnapshot()).toEqual([])
expect(removeLocale).toHaveBeenCalledOnce()
expect(removeBody).toHaveBeenCalledOnce()
})
@@ -0,0 +1,37 @@
/** Worker entry translates parser failures into its private response protocol. */
import { afterEach, expect, it, vi } from 'vitest'
import { Config } from '../src/config.ts'
import { excelFixture } from './excel-fixture.ts'
afterEach(() => { vi.unstubAllGlobals(); vi.resetModules(); vi.doUnmock('../src/client/excel/convert.ts') })
it('returns parsed sheets and bounded failure categories', async () => {
const post = vi.fn()
vi.stubGlobal('postMessage', post)
vi.stubGlobal('onmessage', null)
await import('../src/client/excel/worker.ts')
const handle: (event: MessageEvent) => void = globalThis.onmessage!
const message = (bytes: Uint8Array<ArrayBuffer>, maxCells = 250_000) => {
handle({ data: { bytes, limits: { ...Config({}).excel, maxCells } } } as MessageEvent)
}
message(await excelFixture())
await vi.waitFor(() => { expect(post).toHaveBeenCalledWith(expect.objectContaining({ ok: true })) })
message(new Uint8Array([1, 2]))
await vi.waitFor(() => { expect(post).toHaveBeenCalledWith({ ok: false, code: 'invalid' }) })
message(await excelFixture(), 1)
await vi.waitFor(() => { expect(post).toHaveBeenCalledWith({ ok: false, code: 'tooLarge' }) })
})
it('reports an unexpected parser failure as an invalid workbook', async () => {
const post = vi.fn()
vi.stubGlobal('postMessage', post)
vi.stubGlobal('onmessage', null)
vi.doMock('../src/client/excel/convert.ts', async (importOriginal) => {
const original = await importOriginal<typeof import('../src/client/excel/convert.ts')>()
return { ...original, convertExcel: vi.fn().mockRejectedValue(new Error('parser crashed')) }
})
await import('../src/client/excel/worker.ts')
const handle: (event: MessageEvent) => void = globalThis.onmessage!
handle({ data: { bytes: new Uint8Array(), limits: Config({}).excel } } as MessageEvent)
await vi.waitFor(() => { expect(post).toHaveBeenCalledWith({ ok: false, code: 'invalid' }) })
})
@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path' import { dirname, join, resolve } from 'node:path'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { runInNewContext } from 'node:vm'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
const packageRoot = resolve(import.meta.dirname, '..') const packageRoot = resolve(import.meta.dirname, '..')
@@ -56,13 +57,32 @@ describe('published PDF.js licenses', () => {
const client = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.js'], packageRoot, task.timeout) const client = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.js'], packageRoot, task.timeout)
const pdf = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.pdf.js'], packageRoot, task.timeout) const pdf = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.pdf.js'], packageRoot, task.timeout)
expect([...client.matchAll(/require\.async\("(\.\/client[^"/]*\.js)"\)/gu)].map(match => match[1])) expect([...client.matchAll(/require\.async\("(\.\/client[^"/]*\.js)"\)/gu)].map(match => match[1]))
.toEqual(['./client.pdf.js']) .toEqual(['./client.pdf.js', './client.excel.js'])
expect(client).not.toMatch(/\brequire\("\.\/client[^"/]*\.js"\)/u) expect(client).not.toMatch(/\brequire\("\.\/client[^"/]*\.js"\)/u)
expect([...pdf.matchAll(/require\("(\.\/client[^"/]*\.js)"\)/gu)].map(match => match[1])) expect([...pdf.matchAll(/require\("(\.\/client[^"/]*\.js)"\)/gu)].map(match => match[1]))
.toEqual([]) .toEqual([])
expect(client).not.toContain('//! Bundled PDF.js license notices') expect(client).not.toContain('//! Bundled PDF.js license notices')
expect(client).not.toContain('/pdfjs-dist/') expect(client).not.toContain('/pdfjs-dist/')
expect(pdf).toContain('//! Bundled PDF.js license notices') expect(pdf).toContain('//! Bundled PDF.js license notices')
const excel = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.excel.js'], packageRoot, task.timeout)
expect(excel).not.toMatch(/\brequire\("\.\/client[^"/]*\.js"\)/u)
expect(client).not.toContain('FortuneSheet')
expect(excel).toContain('//! Bundled spreadsheet license notices')
expect(excel).toContain('Copyright (c) 2022 Suzhou Ruilisi Technology Co., Ltd')
expect(excel).toContain('Permission is hereby granted, free of charge')
let initialized = false
runInNewContext(excel, { window: { __ModuleLoader__: { load: (registration: {
factory: (resolve: (specifier: string) => unknown) => { ExcelBody: unknown }
}) => {
const loaded = registration.factory((specifier) => {
if (specifier === '@deepseek-ai/dsh-client-ui-primitives') return {}
if (specifier === 'react' || specifier === 'react/jsx-runtime' || specifier === 'react-dom') return require(specifier) as unknown
throw new Error(`Unexpected browser dependency: ${specifier}`)
})
expect(typeof loaded.ExcelBody).toBe('function')
initialized = true
} } } })
expect(initialized).toBe(true)
const pdfRoot = dirname(require.resolve('pdfjs-dist/package.json')) const pdfRoot = dirname(require.resolve('pdfjs-dist/package.json'))
for (const name of licenseNames) { for (const name of licenseNames) {
const source = readFileSync(join(pdfRoot, name), 'utf8').trimEnd() const source = readFileSync(join(pdfRoot, name), 'utf8').trimEnd()
@@ -1,16 +1,25 @@
import { readFileSync, readdirSync } from 'node:fs' import { readFileSync, readdirSync } from 'node:fs'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import type { UserConfig } from 'tsdown' import { Rolldown, type UserConfig } from 'tsdown'
import { clientBundle } from '../tsdown.client.ts' import { clientBundle } from '../tsdown.client.ts'
const bundle = clientBundle('@deepseek-ai/dsh-client-ui-sidebar-documentpreview', ['lib/types/index.js'], { const bundle = clientBundle('@deepseek-ai/dsh-client-ui-sidebar-documentpreview', ['lib/types/index.js'], {
clientBanner: fileName => fileName.endsWith('client.pdf.js') ? pdfLicenseBanner() : undefined, clientBanner: fileName => fileName.endsWith('client.pdf.js') ? pdfLicenseBanner()
: fileName.endsWith('client.excel.js') ? excelLicenseBanner() : undefined,
}) })
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const workerSpecifier = 'pdfjs-dist/build/pdf.worker.min.mjs?raw' const workerSpecifier = 'pdfjs-dist/build/pdf.worker.min.mjs?raw'
const workerModule = '\0dsh-pdf-worker.mjs' const workerModule = '\0dsh-pdf-worker.mjs'
/** FortuneSheet omits its repository license from the npm payload. */
function excelLicenseBanner(): string {
const fortune = readFileSync(join(import.meta.dirname, 'licenses/FortuneSheet.txt'), 'utf8')
const excel = readFileSync(join(dirname(require.resolve('exceljs/package.json')), 'LICENSE'), 'utf8')
const xml = readFileSync(join(dirname(dirname(require.resolve('fast-xml-parser'))), 'LICENSE'), 'utf8')
return ['//! Bundled spreadsheet license notices', ...`${fortune}\n${excel}\n${xml}`.trimEnd().split('\n').map(line => `// ${line}`)].join('\n')
}
/** License files for PDF.js and the data embedded beside its runtime. */ /** License files for PDF.js and the data embedded beside its runtime. */
function pdfLicenseFiles(root: string): string[] { function pdfLicenseFiles(root: string): string[] {
return ['LICENSE', ...['cmaps', 'standard_fonts', 'wasm'].flatMap(directory => return ['LICENSE', ...['cmaps', 'standard_fonts', 'wasm'].flatMap(directory =>
@@ -53,10 +62,35 @@ const pdfWorker: NonNullable<UserConfig['plugins']> = [{
}, },
}] }]
/** Embed a self-contained browser parser without giving it a loader-module dependency. */
const excelWorker: NonNullable<UserConfig['plugins']> = [{
name: 'dsh-excel-worker-source',
resolveId(source) { return source === './worker.ts?raw' ? '\0dsh-excel-worker-source' : null },
async load(id) {
if (id !== '\0dsh-excel-worker-source') return null
const worker = await Rolldown.rolldown({
input: join(import.meta.dirname, 'src/client/excel/worker.ts'), platform: 'browser',
resolve: { mainFields: ['browser', 'module', 'main'], aliasFields: [['browser']] },
transform: { define: { 'process.env.NODE_ENV': JSON.stringify('production') } },
})
try {
const result = await worker.generate({ format: 'iife', minify: true })
const chunk = result.output[0]
if (chunk?.type !== 'chunk') throw new Error('Excel parser did not emit a JavaScript chunk')
for (const path of Object.keys(chunk.modules)) this.addWatchFile(path)
return `export default ${JSON.stringify(chunk.code)};`
} finally { await worker.close() }
},
}]
export default (options: Parameters<typeof bundle>[0]): UserConfig[] => bundle(options).map(config => export default (options: Parameters<typeof bundle>[0]): UserConfig[] => bundle(options).map(config =>
config.name?.endsWith('/client') === true ? { config.name?.endsWith('/client') === true ? {
...config, ...config,
plugins: [config.plugins, pdfWorker], inputOptions: {
...config.inputOptions,
resolve: { ...config.inputOptions?.resolve, mainFields: ['browser', 'module', 'main'], aliasFields: [['browser']] },
},
plugins: [config.plugins, pdfWorker, excelWorker],
define: { ...config.define, __DSH_PDFJS_ASSETS__: pdfAssets() }, define: { ...config.define, __DSH_PDFJS_ASSETS__: pdfAssets() },
} : config, } : config,
) )
@@ -2953,6 +2953,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
declaredBy: 'an entry in \'sidebar.right.pane.tab\' (client-ui-sidebar-documentpreview), so it exists while that entry is mounted', declaredBy: 'an entry in \'sidebar.right.pane.tab\' (client-ui-sidebar-documentpreview), so it exists while that entry is mounted',
occupants: [ occupants: [
'client-ui-sidebar-documentpreview CodeBody', 'client-ui-sidebar-documentpreview CodeBody',
'client-ui-sidebar-documentpreview LazyExcelBody',
'client-ui-sidebar-documentpreview HtmlBody', 'client-ui-sidebar-documentpreview HtmlBody',
'client-ui-sidebar-documentpreview ImageBody', 'client-ui-sidebar-documentpreview ImageBody',
'client-ui-sidebar-documentpreview MarkdownBody', 'client-ui-sidebar-documentpreview MarkdownBody',
+26
View File
@@ -0,0 +1,26 @@
diff --git a/dist/index.esm.js b/dist/index.esm.js
index c4486a2b38bec4eff10cdfda4d3748d40bfb71ba..f866bc54a2787e3fbe27256ed7513c95393d9e8c 100644
--- a/dist/index.esm.js
+++ b/dist/index.esm.js
@@ -921,7 +921,7 @@ var InputBox = function InputBox() {
return {};
}, [context.luckysheetfile, context.currentSheetId, context.luckysheetCellUpdate, firstSelection]);
useLayoutEffect(function () {
- if (!context.allowEdit) {
+ if (!context.allowEdit && context.forceFormulaRef) {
setContext(function (ctx) {
var flowdata = getFlowdata(ctx);
if (!_.isNil(flowdata) && ctx.forceFormulaRef) {
diff --git a/dist/index.js b/dist/index.js
index e4045518773578e3faaa3a58985a5585f9683088..469b0159ee6e9b308276c971fe9f7e659e229d21 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -932,7 +932,7 @@ var InputBox = function InputBox() {
return {};
}, [context.luckysheetfile, context.currentSheetId, context.luckysheetCellUpdate, firstSelection]);
React.useLayoutEffect(function () {
- if (!context.allowEdit) {
+ if (!context.allowEdit && context.forceFormulaRef) {
setContext(function (ctx) {
var flowdata = core.getFlowdata(ctx);
if (!___default['default'].isNil(flowdata) && ctx.forceFormulaRef) {
+456
View File
@@ -12,6 +12,7 @@ overrides:
patchedDependencies: patchedDependencies:
'@earendil-works/[email protected]': b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f '@earendil-works/[email protected]': b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f
'@electron/[email protected]': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861 '@electron/[email protected]': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861
'@fortune-sheet/[email protected]': a278da43acb4f5f15b3703592688b952db02aeedb1e6900d20dbd5db38d4cb38
'@yao-pkg/[email protected]': 75891fc22b277c145cafeab83e41baf4a70f5b8fe6dc0801c6a0b922d7ee88c7 '@yao-pkg/[email protected]': 75891fc22b277c145cafeab83e41baf4a70f5b8fe6dc0801c6a0b922d7ee88c7
[email protected]: b40ae545608897914bd25fb009c97eeac478c34e8a910298ddcb01b746534bb0 [email protected]: b40ae545608897914bd25fb009c97eeac478c34e8a910298ddcb01b746534bb0
@@ -4540,6 +4541,12 @@ importers:
'@deepseek-ai/dsh-util-workspace-path': '@deepseek-ai/dsh-util-workspace-path':
specifier: workspace:^ specifier: workspace:^
version: link:../../util/workspace-path version: link:../../util/workspace-path
'@fortune-sheet/core':
specifier: 1.0.4
version: 1.0.4
'@fortune-sheet/react':
specifier: 1.0.4
version: 1.0.4(patch_hash=a278da43acb4f5f15b3703592688b952db02aeedb1e6900d20dbd5db38d4cb38)([email protected]([email protected]))([email protected])
'@testing-library/react': '@testing-library/react':
specifier: ^16.1.0 specifier: ^16.1.0
version: 16.3.2(@testing-library/[email protected])(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]) version: 16.3.2(@testing-library/[email protected])(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -4555,6 +4562,15 @@ importers:
dompurify: dompurify:
specifier: ^3.4.11 specifier: ^3.4.11
version: 3.4.11 version: 3.4.11
exceljs:
specifier: 4.4.0
version: 4.4.0
fast-xml-parser:
specifier: 5.11.1
version: 5.11.1
fflate:
specifier: ^0.8.3
version: 0.8.3
pdfjs-dist: pdfjs-dist:
specifier: 6.3.289 specifier: 6.3.289
version: 6.3.289 version: 6.3.289
@@ -14196,6 +14212,28 @@ packages:
'@noble/hashes': '@noble/hashes':
optional: true optional: true
'@fast-csv/[email protected]':
resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==}
'@fast-csv/[email protected]':
resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==}
'@formulajs/[email protected]':
resolution: {integrity: sha512-WpgiuJaBl/Hcda9Ti8a7mlnw/vUZkJrtjABvojz6P6mo6d8EscudyA7iAt/kTLsgi0c8zfmzQd/Yjb4ASFkw+g==}
hasBin: true
'@fortune-sheet/[email protected]':
resolution: {integrity: sha512-CDnUVebfvtT++CymNRw0qnY4sz6zYO3KZazlH+nG6otkRkZ05Bvt7NXqVhmKKSSxIvJR4R6h50abu/dVGBpjpw==}
'@fortune-sheet/[email protected]':
resolution: {integrity: sha512-za2ZVQ5ZfMSPCtL8MdqhCthPip7AoeU4MPyd5UDo4RCl9/arrv1Jz0y755mACVVi4suSZxrzWoJGDkLmofoSmw==}
'@fortune-sheet/[email protected]':
resolution: {integrity: sha512-v+BU5mmp2hhUe4gRF+vOJ3j8zZkzHU0kzhTZzp+Nn00wA/RArMr+CO+SAluKlCPAsTZgPwmgOEK685WPqJ5XNw==}
peerDependencies:
react: '>= 18.2'
react-dom: '>= 18.2'
'@google/[email protected]': '@google/[email protected]':
resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -14749,6 +14787,9 @@ packages:
'@nodable/[email protected]': '@nodable/[email protected]':
resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==}
'@nodable/[email protected]':
resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==}
'@octokit/[email protected]': '@octokit/[email protected]':
resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==} resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==}
@@ -15948,6 +15989,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/[email protected]':
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
@@ -15989,6 +16033,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==} resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==}
'@types/[email protected]':
resolution: {integrity: sha512-jjKoBekfYDH331060tZhosdJVDnXIXx+T8Iw2h2T4HEds6Ddb2lr0JxD15+XPKlXwRHRNgZoY+4Fb2ykoqzHBg==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
@@ -16408,6 +16455,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==} resolution: {integrity: sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==}
[email protected]:
resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==}
[email protected]: [email protected]:
resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -16415,6 +16465,18 @@ packages:
dmg-builder: 26.15.3 dmg-builder: 26.15.3
electron-builder-squirrel-windows: 26.15.3 electron-builder-squirrel-windows: 26.15.3
[email protected]:
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
engines: {node: '>= 6'}
[email protected]:
resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==}
engines: {node: '>= 10'}
[email protected]:
resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==}
engines: {node: '>= 10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -16530,12 +16592,23 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
[email protected]:
resolution: {integrity: sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw==}
engines: {node: '>=0.8'}
[email protected]: [email protected]:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
[email protected]:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
[email protected]: [email protected]:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
[email protected]:
resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
[email protected]: [email protected]:
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
@@ -16545,6 +16618,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
[email protected]:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
@@ -16577,6 +16653,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
[email protected]:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
@@ -16587,12 +16666,20 @@ packages:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
[email protected]:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
[email protected]:
resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
engines: {node: '>=0.2.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -16650,6 +16737,9 @@ packages:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'} engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -16762,6 +16852,10 @@ packages:
resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
[email protected]:
resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==}
engines: {node: '>= 10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -16833,6 +16927,15 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
[email protected]:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==}
engines: {node: '>= 10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==}
@@ -17378,6 +17481,10 @@ packages:
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
[email protected]:
resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
engines: {node: '>=8.3.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-Cxl6MKxB1dr1H0FHmiizJ+lavKF7pV+fcDZFyqMB8d5m7qUPm/OtZYcD5vPWePKxSnTQ57KuBd9mtdZ3oNCvyQ==} resolution: {integrity: sha512-Cxl6MKxB1dr1H0FHmiizJ+lavKF7pV+fcDZFyqMB8d5m7qUPm/OtZYcD5vPWePKxSnTQ57KuBd9mtdZ3oNCvyQ==}
engines: {node: '>=22'} engines: {node: '>=22'}
@@ -17419,6 +17526,10 @@ packages:
resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==} resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==}
engines: {node: '>=12.17.0'} engines: {node: '>=12.17.0'}
[email protected]:
resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
engines: {node: '>=10.0.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -17440,6 +17551,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==}
[email protected]:
resolution: {integrity: sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==}
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==}
hasBin: true hasBin: true
@@ -17578,6 +17693,11 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] os: [darwin]
[email protected]:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
deprecated: This package is no longer supported.
[email protected]: [email protected]:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -17801,6 +17921,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
[email protected]:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
@@ -17896,6 +18019,9 @@ packages:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -18068,6 +18194,9 @@ packages:
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
engines: {node: '>=0.6.0'} engines: {node: '>=0.6.0'}
[email protected]:
resolution: {integrity: sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==}
[email protected]: [email protected]:
resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -18103,6 +18232,10 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
[email protected]:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
[email protected]: [email protected]:
resolution: {integrity: sha512-119HryNcvr4nqn0wUIrNPgpMEPn9yMQzEcW/lezRsnb56PCJriJB92+MCySPVcWDxJnZef7o0T3jdnPNiSH7Qg==} resolution: {integrity: sha512-119HryNcvr4nqn0wUIrNPgpMEPn9yMQzEcW/lezRsnb56PCJriJB92+MCySPVcWDxJnZef7o0T3jdnPNiSH7Qg==}
cpu: [arm64] cpu: [arm64]
@@ -18246,6 +18379,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
[email protected]:
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -18253,16 +18389,49 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
[email protected]:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
[email protected]:
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
[email protected]:
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
[email protected]:
resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
[email protected]:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
[email protected]: [email protected]:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
[email protected]:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
[email protected]:
resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==}
[email protected]:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
[email protected]:
resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
[email protected]:
resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
[email protected]:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@@ -18683,6 +18852,10 @@ packages:
engines: {node: ^20.17.0 || >=22.9.0} engines: {node: ^20.17.0 || >=22.9.0}
hasBin: true hasBin: true
[email protected]:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -18691,6 +18864,9 @@ packages:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'} engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
@@ -18826,6 +19002,10 @@ packages:
resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
[email protected]:
resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==}
engines: {node: '>=14.0.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -19076,6 +19256,9 @@ packages:
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
[email protected]:
resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
@@ -19088,6 +19271,9 @@ packages:
resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
[email protected]:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
[email protected]: [email protected]:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
@@ -19228,6 +19414,10 @@ packages:
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
engines: {node: '>=11.0.0'} engines: {node: '>=11.0.0'}
[email protected]:
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
engines: {node: '>=10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'} engines: {node: '>=v12.22.7'}
@@ -19462,6 +19652,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==} resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==}
[email protected]:
resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==}
[email protected]: [email protected]:
resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==}
@@ -19524,6 +19717,9 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
[email protected]:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
[email protected]: [email protected]:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@@ -19578,6 +19774,9 @@ packages:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'} engines: {node: '>=20'}
[email protected]:
resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true hasBin: true
@@ -19767,6 +19966,9 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
[email protected]:
resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==}
[email protected]: [email protected]:
resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==}
@@ -19800,6 +20002,11 @@ packages:
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true hasBin: true
[email protected]:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
[email protected]: [email protected]:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -20124,6 +20331,10 @@ packages:
resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
[email protected]:
resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
engines: {node: '>=16.0.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -20186,6 +20397,10 @@ packages:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'} engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
engines: {node: '>= 10'}
[email protected]: [email protected]:
resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
peerDependencies: peerDependencies:
@@ -21291,6 +21506,54 @@ snapshots:
optionalDependencies: optionalDependencies:
'@noble/hashes': 2.3.0 '@noble/hashes': 2.3.0
'@fast-csv/[email protected]':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.isboolean: 3.0.3
lodash.isequal: 4.5.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
'@fast-csv/[email protected]':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.groupby: 4.6.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
lodash.isundefined: 3.0.1
lodash.uniq: 4.5.0
'@formulajs/[email protected]':
dependencies:
bessel: 1.0.2
jstat: 1.9.6
'@fortune-sheet/[email protected]':
dependencies:
'@fortune-sheet/formula-parser': 0.2.13
dayjs: 1.11.21
immer: 9.0.21
lodash: 4.18.1
numeral: 2.0.6
uuid: 8.3.2
'@fortune-sheet/[email protected]':
dependencies:
'@formulajs/formulajs': 2.9.3
tiny-emitter: 2.1.0
'@fortune-sheet/[email protected](patch_hash=a278da43acb4f5f15b3703592688b952db02aeedb1e6900d20dbd5db38d4cb38)([email protected]([email protected]))([email protected])':
dependencies:
'@fortune-sheet/core': 1.0.4
'@types/regenerator-runtime': 0.13.8
immer: 9.0.21
lodash: 4.18.1
react: 18.3.1
react-dom: 18.3.1([email protected])
regenerator-runtime: 0.14.1
'@google/[email protected](@modelcontextprotocol/[email protected]([email protected]))': '@google/[email protected](@modelcontextprotocol/[email protected]([email protected]))':
dependencies: dependencies:
google-auth-library: 10.7.0 google-auth-library: 10.7.0
@@ -21794,6 +22057,8 @@ snapshots:
'@nodable/[email protected]': {} '@nodable/[email protected]': {}
'@nodable/[email protected]': {}
'@octokit/[email protected]': {} '@octokit/[email protected]': {}
'@octokit/[email protected]': {} '@octokit/[email protected]': {}
@@ -22775,6 +23040,8 @@ snapshots:
'@types/node': 22.20.0 '@types/node': 22.20.0
form-data: 4.0.6 form-data: 4.0.6
'@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
@@ -22818,6 +23085,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.20.0 '@types/node': 22.20.0
'@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
dependencies: dependencies:
'@types/node': 22.20.0 '@types/node': 22.20.0
@@ -23277,6 +23546,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]([email protected])([email protected]): [email protected]([email protected])([email protected]):
dependencies: dependencies:
'@electron/asar': 3.4.1 '@electron/asar': 3.4.1
@@ -23325,6 +23596,42 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
[email protected]:
dependencies:
glob: 7.2.3
graceful-fs: 4.2.11
lazystream: 1.0.1
lodash.defaults: 4.2.0
lodash.difference: 4.5.0
lodash.flatten: 4.4.0
lodash.isplainobject: 4.0.6
lodash.union: 4.6.0
normalize-path: 3.0.0
readable-stream: 2.3.8
[email protected]:
dependencies:
glob: 7.2.3
graceful-fs: 4.2.11
lazystream: 1.0.1
lodash.defaults: 4.2.0
lodash.difference: 4.5.0
lodash.flatten: 4.4.0
lodash.isplainobject: 4.0.6
lodash.union: 4.6.0
normalize-path: 3.0.0
readable-stream: 3.6.2
[email protected]:
dependencies:
archiver-utils: 2.1.0
async: 3.2.6
buffer-crc32: 0.2.13
readable-stream: 3.6.2
readdir-glob: 1.1.3
tar-stream: 2.2.0
zip-stream: 4.1.1
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -23416,12 +23723,21 @@ snapshots:
dependencies: dependencies:
tweetnacl: 0.14.5 tweetnacl: 0.14.5
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
require-from-string: 2.0.2 require-from-string: 2.0.2
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -23432,6 +23748,8 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 readable-stream: 3.6.2
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -23478,6 +23796,8 @@ snapshots:
node-releases: 2.0.51 node-releases: 2.0.51
update-browserslist-db: 1.2.3([email protected]) update-browserslist-db: 1.2.3([email protected])
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -23486,6 +23806,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.20.0 '@types/node': 22.20.0
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
base64-js: 1.5.1 base64-js: 1.5.1
@@ -23496,6 +23818,8 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
debug: 4.4.3([email protected]) debug: 4.4.3([email protected])
@@ -23564,6 +23888,10 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
traverse: 0.3.9
[email protected]: [email protected]:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@@ -23645,6 +23973,13 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
buffer-crc32: 0.2.13
crc32-stream: 4.0.3
normalize-path: 3.0.0
readable-stream: 3.6.2
[email protected]: [email protected]:
dependencies: dependencies:
mime-db: 1.54.0 mime-db: 1.54.0
@@ -23729,6 +24064,13 @@ snapshots:
dependencies: dependencies:
layout-base: 2.0.1 layout-base: 2.0.1
[email protected]: {}
[email protected]:
dependencies:
crc-32: 1.2.2
readable-stream: 3.6.2
[email protected]: [email protected]:
optional: true optional: true
@@ -24411,6 +24753,18 @@ snapshots:
dependencies: dependencies:
eventsource-parser: 3.1.0 eventsource-parser: 3.1.0
[email protected]:
dependencies:
archiver: 5.3.2
dayjs: 1.11.21
fast-csv: 4.3.6
jszip: 3.10.1
readable-stream: 3.6.2
saxes: 5.0.1
tmp: 0.2.7
unzipper: 0.10.14
uuid: 8.3.2
[email protected]: [email protected]:
dependencies: dependencies:
'@sindresorhus/merge-streams': 4.0.0 '@sindresorhus/merge-streams': 4.0.0
@@ -24489,6 +24843,11 @@ snapshots:
dependencies: dependencies:
pure-rand: 8.4.0 pure-rand: 8.4.0
[email protected]:
dependencies:
'@fast-csv/format': 4.3.5
'@fast-csv/parse': 4.3.6
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -24506,6 +24865,15 @@ snapshots:
path-expression-matcher: 1.5.0 path-expression-matcher: 1.5.0
xml-naming: 0.1.0 xml-naming: 0.1.0
[email protected]:
dependencies:
'@nodable/entities': 3.0.0
fast-xml-builder: 1.2.0
is-unsafe: 2.0.2
path-expression-matcher: 1.6.2
strnum: 2.4.2
xml-naming: 0.3.0
[email protected]: [email protected]:
dependencies: dependencies:
'@nodable/entities': 2.2.0 '@nodable/entities': 2.2.0
@@ -24665,6 +25033,13 @@ snapshots:
[email protected]: [email protected]:
optional: true optional: true
[email protected]:
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
mkdirp: 0.5.6
rimraf: 2.6.3
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -24961,6 +25336,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -25020,6 +25397,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -25180,6 +25559,8 @@ snapshots:
json-schema: 0.4.0 json-schema: 0.4.0
verror: 1.10.0 verror: 1.10.0
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -25234,6 +25615,10 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
readable-stream: 2.3.8
[email protected]: [email protected]:
optional: true optional: true
@@ -25341,18 +25726,42 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -25943,6 +26352,8 @@ snapshots:
dependencies: dependencies:
abbrev: 4.0.0 abbrev: 4.0.0
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -25950,6 +26361,8 @@ snapshots:
path-key: 4.0.0 path-key: 4.0.0
unicorn-magic: 0.3.0 unicorn-magic: 0.3.0
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -26104,6 +26517,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -26362,6 +26777,10 @@ snapshots:
process: 0.11.10 process: 0.11.10
string_decoder: 1.3.0 string_decoder: 1.3.0
[email protected]:
dependencies:
minimatch: 5.1.9
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -26370,6 +26789,8 @@ snapshots:
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
regex-utilities: 2.3.0 regex-utilities: 2.3.0
@@ -26562,6 +26983,10 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
xmlchars: 2.2.0
[email protected]: [email protected]:
dependencies: dependencies:
xmlchars: 2.2.0 xmlchars: 2.2.0
@@ -26855,6 +27280,10 @@ snapshots:
dependencies: dependencies:
anynum: 1.0.0 anynum: 1.0.0
[email protected]:
dependencies:
anynum: 1.0.1
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -26952,6 +27381,8 @@ snapshots:
dependencies: dependencies:
semver: 5.7.2 semver: 5.7.2
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -26998,6 +27429,8 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -27158,6 +27591,19 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
big-integer: 1.6.52
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.11
listenercount: 1.0.1
readable-stream: 2.3.8
setimmediate: 1.0.5
[email protected]: [email protected]:
dependencies: dependencies:
bluebird: 3.7.2 bluebird: 3.7.2
@@ -27193,6 +27639,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: [email protected]:
@@ -27541,6 +27989,8 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -27598,6 +28048,12 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
archiver-utils: 3.0.4
compress-commons: 4.1.2
readable-stream: 3.6.2
[email protected]([email protected]): [email protected]([email protected]):
dependencies: dependencies:
zod: 4.4.3 zod: 4.4.3
+1
View File
@@ -97,5 +97,6 @@ minimumReleaseAgeExclude:
patchedDependencies: patchedDependencies:
'@earendil-works/[email protected]': patches/@[email protected] '@earendil-works/[email protected]': patches/@[email protected]
'@electron/[email protected]': patches/@[email protected] '@electron/[email protected]': patches/@[email protected]
'@fortune-sheet/[email protected]': patches/@[email protected]
'@yao-pkg/[email protected]': patches/@[email protected] '@yao-pkg/[email protected]': patches/@[email protected]
[email protected]: patches/[email protected] [email protected]: patches/[email protected]
@@ -72,10 +72,14 @@
- Binary text shown: false - Binary text shown: false
- Plain-text option and viewer picker: hidden - Plain-text option and viewer picker: hidden
## Spreadsheet preview ## Browser Excel preview
- XLS: unsupported / Open in default app - Opens without the Office conversion service
- XLSX: unsupported / Open in default app - Sheets: 季度预算 | 公式与格式; hidden worksheet omitted
- Cached XLOOKUP result copied: 42; typing leaves it unchanged
- Formula bar is read-only; PDF body and editing toolbar absent
- Legacy guidance: Save this legacy Excel file as .xlsx to preview it.
- Invalid XLSX: This Excel file could not be opened. It may be damaged or password protected.
## Unknown suffix ## Unknown suffix