@4399ywkf/editor 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -11
- package/dist/PptxView-ZH4FZKNO.js +3 -0
- package/dist/PptxView-ZH4FZKNO.js.map +1 -0
- package/dist/SheetView-SDBRSTVS.js +3 -0
- package/dist/{SheetView-DSADIZNS.js.map → SheetView-SDBRSTVS.js.map} +1 -1
- package/dist/chunk-5HMKRNIH.js +27 -0
- package/dist/chunk-5HMKRNIH.js.map +1 -0
- package/dist/chunk-5XG4YMAB.js +149 -0
- package/dist/chunk-5XG4YMAB.js.map +1 -0
- package/dist/chunk-DGUNVCOM.js +711 -0
- package/dist/chunk-DGUNVCOM.js.map +1 -0
- package/dist/chunk-HPSPOM2O.js +128 -0
- package/dist/chunk-HPSPOM2O.js.map +1 -0
- package/dist/chunk-KRP5V7VF.js +130 -0
- package/dist/chunk-KRP5V7VF.js.map +1 -0
- package/dist/chunk-SBTKDJAL.js +107 -0
- package/dist/chunk-SBTKDJAL.js.map +1 -0
- package/dist/{chunk-L6ZR5HHW.js → chunk-SYCK6Y5F.js} +15 -10
- package/dist/chunk-SYCK6Y5F.js.map +1 -0
- package/dist/chunk-WF7PIIFR.js +21 -0
- package/dist/chunk-WF7PIIFR.js.map +1 -0
- package/dist/csv-parse-D0zTBvDB.d.ts +216 -0
- package/dist/csv.d.ts +86 -0
- package/dist/csv.js +34 -0
- package/dist/csv.js.map +1 -0
- package/dist/docx.d.ts +15 -0
- package/dist/docx.js +61 -3
- package/dist/docx.js.map +1 -1
- package/dist/import-GVZDLWA2.js +6 -0
- package/dist/import-GVZDLWA2.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts +1 -1
- package/dist/pptx.d.ts +36 -0
- package/dist/pptx.js +3 -0
- package/dist/pptx.js.map +1 -0
- package/dist/{registry-BBL8yeIT.d.ts → registry-XJFJXtZ_.d.ts} +6 -0
- package/dist/sheet-runtime.d.ts +212 -0
- package/dist/sheet-runtime.js +416 -0
- package/dist/sheet-runtime.js.map +1 -0
- package/dist/sheet.d.ts +7 -5
- package/dist/sheet.js +1 -1
- package/dist/xlsx.d.ts +127 -189
- package/dist/xlsx.js +8 -104
- package/dist/xlsx.js.map +1 -1
- package/package.json +19 -3
- package/dist/SheetView-DSADIZNS.js +0 -3
- package/dist/chunk-L6ZR5HHW.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @4399ywkf/editor
|
|
2
2
|
|
|
3
|
-
4399 多格式文档编辑器。一个包覆盖 **doc / sheet / pdf**,引擎按需下载。
|
|
3
|
+
4399 多格式文档编辑器。一个包覆盖 **doc / sheet / pdf / pptx**,引擎按需下载。
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @4399ywkf/editor
|
|
@@ -20,13 +20,19 @@ import "@4399ywkf/editor/styles"
|
|
|
20
20
|
| **doc**(含云文档) | tiptap | ✅ | ✅ | ✅ | 内置 |
|
|
21
21
|
| **sheet** | Univer OSS 核心 | ✅ | ✅ | ✕ | 可选 peer `@univerjs/presets` |
|
|
22
22
|
| **pdf** | pdf.js | ✕ 只读 | ✕ | ✕ | 可选 peer `pdfjs-dist` |
|
|
23
|
-
| **pptx** |
|
|
23
|
+
| **pptx** | pptx-preview | ✕ 只读 | ✕ | ✕ | 可选 peer `pptx-preview` |
|
|
24
24
|
|
|
25
25
|
云文档与本地 doc 是**同一套 tiptap**,不做区分。
|
|
26
26
|
|
|
27
|
+
「AI 可操作」指的是有结构化操作面可挂:doc 走 [`./doc-runtime`](#ai-操作面),
|
|
28
|
+
sheet 走 `./sheet-runtime`,工具定义在 [`@4399ywkf/editor-mcp`](../editor-mcp)。
|
|
29
|
+
|
|
30
|
+
导入导出都**在本地转换,文档不出内网**:docx ⇄ doc、xlsx/csv/tsv ⇄ sheet,
|
|
31
|
+
不引 SheetJS、不依赖任何转换服务。
|
|
32
|
+
|
|
27
33
|
## 为什么引擎是懒加载的
|
|
28
34
|
|
|
29
|
-
主入口只有 **3KB**(格式路由),tiptap 的 698KB 在独立 chunk 里,Univer / pdf.js
|
|
35
|
+
主入口只有 **3KB**(格式路由),tiptap 的 698KB 在独立 chunk 里,Univer / pdf.js / pptx-preview 都是可选 peer。
|
|
30
36
|
`import "@4399ywkf/editor"` 不会把任何引擎拉进你的主包 —— 只有真的渲染到某种格式时才下载。
|
|
31
37
|
|
|
32
38
|
Univer sheets 核心 gzip 后 871KB;无条件打进主包会直接毁掉阅读侧首屏。
|
|
@@ -36,12 +42,59 @@ Univer sheets 核心 gzip 后 871KB;无条件打进主包会直接毁掉阅读
|
|
|
36
42
|
```ts
|
|
37
43
|
import { NotionEditor, ArticleViewer } from "@4399ywkf/editor/doc"
|
|
38
44
|
import { ImportDocx, ExportDocx } from "@4399ywkf/editor/docx"
|
|
39
|
-
import { downloadXlsx } from "@4399ywkf/editor/xlsx"
|
|
45
|
+
import { downloadXlsx, importSheetSource } from "@4399ywkf/editor/xlsx"
|
|
46
|
+
import { downloadCsv } from "@4399ywkf/editor/csv"
|
|
40
47
|
import { SheetView } from "@4399ywkf/editor/sheet"
|
|
41
48
|
import { PdfView } from "@4399ywkf/editor/pdf"
|
|
49
|
+
import { PptxView, repairPptx } from "@4399ywkf/editor/pptx"
|
|
50
|
+
import { DocRuntime } from "@4399ywkf/editor/doc-runtime" // AI 可驱动的文档操作面
|
|
51
|
+
import { SheetRuntime } from "@4399ywkf/editor/sheet-runtime" // 同上,表格版
|
|
42
52
|
import "@4399ywkf/editor/styles"
|
|
43
53
|
```
|
|
44
54
|
|
|
55
|
+
## pptx 只读预览
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm add pptx-preview # 可选 peer,不装就落到「暂不支持」兜底
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<DocumentView format="pptx" source={file} onError={(e) => toast(e.message)} />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**定位是「看清这页讲什么」,不是所见即所得。** 申报也照此写死:
|
|
66
|
+
`editable: false`、`aiOperable: false`。已知差距,接之前请先认下:
|
|
67
|
+
|
|
68
|
+
- **图表渲染不出数据** —— 轴画得出来,柱子/折线是空的,系列名和标题退化成
|
|
69
|
+
「系列1 / 图表标题」。该库打包了 ECharts,却没把 `c:cat` / `c:val` 接上去
|
|
70
|
+
- 缺字体时替换成系统字体,观感和原稿不一致
|
|
71
|
+
- 内部偶有 SVG 路径算出 NaN,个别形状画不出来
|
|
72
|
+
|
|
73
|
+
只认 OOXML:`.pptx` `.ppsx` `.potx` `.pptm` `.ppsm` `.potm`。
|
|
74
|
+
`.ppt` / `.pps` / `.pot` 是二进制复合文档、不是 zip,该库解不了,
|
|
75
|
+
所以**不申报** —— 让它落到「暂不支持」比装作能开要好。
|
|
76
|
+
|
|
77
|
+
### 为什么要先修一道
|
|
78
|
+
|
|
79
|
+
真实文件里大半份的 `[Content_Types].xml` 声明了 zip 中并不存在的 slideMaster。
|
|
80
|
+
PowerPoint 自己不介意,`pptx-preview` 会**整份静默失败**:不抛错、不报警,
|
|
81
|
+
`slideCount` 就是 0,面板一片空白。
|
|
82
|
+
|
|
83
|
+
17 份真实文件实测:**裸接只有 5 份能打开,内置 `repairPptx` 后 17 份全开**。
|
|
84
|
+
`PptxView` 已经自动做了这一道;直接用 `pptx-preview` 的宿主请自己先过:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { repairPptx } from "@4399ywkf/editor/pptx"
|
|
88
|
+
const { bytes, removed } = repairPptx(new Uint8Array(await file.arrayBuffer()))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
只在真有幽灵声明时才解压重打包(用已有的 fflate,不引 JSZip),
|
|
92
|
+
文件没问题就原样返回入参。17 份文件逐条目做过 sha256 比对:
|
|
93
|
+
除 `[Content_Types].xml` 外**字节全等**。
|
|
94
|
+
|
|
95
|
+
引擎选型与这道修复都对齐腾讯 WorkBuddy 内置的 pptx 预览。要更高保真,
|
|
96
|
+
用同名 `registerFormat` 覆盖本通道即可(见下)。
|
|
97
|
+
|
|
45
98
|
## 表格通道配置
|
|
46
99
|
|
|
47
100
|
Univer 的 UI 是 `univer-` 前缀的原子类,没有稳定选择器 —— **别用 CSS 硬藏工具栏**。
|
|
@@ -125,6 +178,77 @@ const { bytes, warnings, stats } = xlsxFromSnapshot(snapshot)
|
|
|
125
178
|
由 Excel 打开时重算 —— 这时会自动加上 `fullCalcOnLoad`,否则不重算的消费方
|
|
126
179
|
(网页预览、pandas)会看到一片空白。
|
|
127
180
|
|
|
181
|
+
## csv 导出
|
|
182
|
+
|
|
183
|
+
单独子路径,**一个依赖都不需要**(xlsx 那边要 fflate 打 zip),产物 6.9KB:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { downloadCsv, exportCsv } from "@4399ywkf/editor/csv"
|
|
187
|
+
|
|
188
|
+
const { warnings } = downloadCsv(univerAPI, "明细.csv")
|
|
189
|
+
if (warnings.length) toast(warnings.join("\n"))
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**csv 是单表格式,工作簿是多表的** —— 只导一张,其余的在 `warnings` 里逐个点名:
|
|
193
|
+
|
|
194
|
+
> csv 是单表格式,只导出了「明细」。未导出的 1 张表:「汇总」
|
|
195
|
+
|
|
196
|
+
传 `univerAPI` 时导**真正的活动表**;只传快照时导第一张 —— 快照里根本没有
|
|
197
|
+
「活动表」这个字段,只有运行时实例问得出来。要指定就用 `options.sheet`(表 id 或表名)。
|
|
198
|
+
|
|
199
|
+
默认加 **UTF-8 BOM**:不加的话 Excel 会按本地编码(简中系统是 GBK)解析,中文全是乱码。
|
|
200
|
+
转义按 RFC 4180,含分隔符/引号/换行的字段加引号、内部引号翻倍。
|
|
201
|
+
`delimiter: "\t"` 就是 tsv。
|
|
202
|
+
|
|
203
|
+
其余降级同样会点名:合并单元格被拆开、富文本压平、公式写成算好的值
|
|
204
|
+
(`formulas: "formula"` 可改成写公式原文)。**日期不做转换** —— 它在 Univer 里是
|
|
205
|
+
「序列号 + 数字格式」,csv 里会写成 `45678`。格式码有转义、区域前缀、还有和 `mm`
|
|
206
|
+
同形的分钟,猜错日期比原样给出序列号更糟,所以只警告不转换。
|
|
207
|
+
|
|
208
|
+
## xlsx / csv / tsv 导入
|
|
209
|
+
|
|
210
|
+
导出的反方向,同一套约定:**本地解析,文档不出内网**,不引 SheetJS。
|
|
211
|
+
路由层直接给 `source` 就行,`File` / `Blob` / URL 都收:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
<DocumentView
|
|
215
|
+
format="sheet"
|
|
216
|
+
source={file}
|
|
217
|
+
onWarnings={(w) => toast(w.join("\n"))} // ← 别吞,见下
|
|
218
|
+
onError={(e) => toast(e.message)}
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
也可以只要快照,自己塞给 Univer:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { importSheetSource } from "@4399ywkf/editor/xlsx"
|
|
226
|
+
|
|
227
|
+
const { workbook, warnings, format } = await importSheetSource(file)
|
|
228
|
+
univerAPI.createWorkbook(workbook)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Node 侧没有全局 `DOMParser`,把 `@xmldom/xmldom` 的传进去即可
|
|
232
|
+
(`importXlsx(bytes, { DOMParser })`)—— 解析层是纯函数,整条链路不碰 DOM。
|
|
233
|
+
|
|
234
|
+
**能保住的**:多工作表(顺序 / 隐藏)、值与类型、公式(普通 / 共享 / 数组)、
|
|
235
|
+
数字格式(含内建表)、字体 / 颜色 / 填充 / 边框 / 对齐 / 换行 / 旋转、合并单元格、
|
|
236
|
+
行高列宽、隐藏行列、冻结窗格、网格线、标签颜色、`quotePrefix`(强制文本)。
|
|
237
|
+
|
|
238
|
+
**保不住的逐个点名**(进 `warnings`):图片 / 图表、透视表、条件格式、数据校验、
|
|
239
|
+
筛选、超链接、批注、宏、主题色、1904 日期系统。和导出侧一样 ——
|
|
240
|
+
**打开文件才发现少了东西,远比导入时被告知糟糕**。
|
|
241
|
+
|
|
242
|
+
`.xls` 是老的二进制复合文档,明确不解析:报错会直接告诉用户「另存为 .xlsx」,
|
|
243
|
+
而不是给一个空表。
|
|
244
|
+
|
|
245
|
+
csv / tsv 两处有立场的处理:
|
|
246
|
+
|
|
247
|
+
- **编码嗅探**:UTF-8 严格解码优先,失败退 GBK(简中环境相当一部分 csv 是 Excel
|
|
248
|
+
直接另存的),退了会在 `warnings` 里说明。
|
|
249
|
+
- **类型推断保守**:前导零(`0123`)和超过 15 位有效数字的「数字」保留为强制文本 ——
|
|
250
|
+
转成 number 会失真,宁可留原文也不悄悄改值。订单号、身份证号就是这么被毁掉的。
|
|
251
|
+
|
|
128
252
|
## docx 导入导出
|
|
129
253
|
|
|
130
254
|
API 形态对齐 `@tiptap-pro/extension-import-docx` / `-export-docx`,但**在本地解析**,
|
|
@@ -151,22 +275,51 @@ editor.chain().exportDocx({ onCompleteExport: (blob) => save(blob) }).run()
|
|
|
151
275
|
|
|
152
276
|
已知缺口:`fldChar` 复杂域、修订痕迹、页眉页脚 / 脚注、超链接导出未建外部关系。
|
|
153
277
|
|
|
278
|
+
## AI 操作面
|
|
279
|
+
|
|
280
|
+
要让 AI 改**用户此刻正在看的那个实例**(而不是复制一份文本出去改),
|
|
281
|
+
挂对应的 runtime 即可。两层都只认编辑器实例,**不知道 MCP / HTTP 存在**:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { DocRuntime } from "@4399ywkf/editor/doc-runtime"
|
|
285
|
+
import { SheetRuntime } from "@4399ywkf/editor/sheet-runtime"
|
|
286
|
+
|
|
287
|
+
const doc = useMemo(() => new DocRuntime(), [])
|
|
288
|
+
const sheet = useMemo(() => new SheetRuntime(), [])
|
|
289
|
+
|
|
290
|
+
<NotionEditor onEditorReady={(editor) => doc.setEditor(editor)} />
|
|
291
|
+
<DocumentView format="sheet" onReady={(api) => sheet.setUniverAPI(api)} />
|
|
292
|
+
|
|
293
|
+
await doc.call("doc_replace_text", { ... }) // 两个 runtime 的 call 是同构的
|
|
294
|
+
await sheet.call("sheet_read", { range: "A1:D20" })
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- `doc-runtime`:LiteXML(对 ProseMirror JSON 的有损压缩,省 1.55x 以上 token)+ 8 类操作。
|
|
298
|
+
- `sheet-runtime`:读走快照(CSV 文本,比 JSON 单元格矩阵省几倍 token),
|
|
299
|
+
写走 Univer facade 的 command 体系(这样才有撤销栈和协同广播)。
|
|
300
|
+
|
|
301
|
+
工具定义(name / JSON Schema / description)和传输(浏览器桥 / hub / stdio MCP)
|
|
302
|
+
都在 [`@4399ywkf/editor-mcp`](../editor-mcp),本包一行传输代码都没有。
|
|
303
|
+
|
|
154
304
|
## 注册自有格式
|
|
155
305
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
306
|
+
内置通道覆盖不了、或保真度不够时,宿主可以用同名 `registerFormat` 直接覆盖。
|
|
307
|
+
|
|
308
|
+
pptx **编辑**就属于这种:Univer Slides 官方标注「请勿用于生产环境」,开源世界没有
|
|
309
|
+
第二个可用的前端 pptx 编辑引擎;真要做只有 OnlyOffice / Collabora 那一档
|
|
310
|
+
(AGPL,需法务口径 + 独立部署)。所以内置的只有只读预览,要更高保真自己接:
|
|
159
311
|
|
|
160
312
|
```ts
|
|
161
313
|
import { registerFormat } from "@4399ywkf/editor"
|
|
162
314
|
|
|
315
|
+
// 同名注册会覆盖内置通道
|
|
163
316
|
registerFormat({
|
|
164
317
|
format: "pptx",
|
|
165
318
|
label: "演示文稿",
|
|
166
|
-
extensions: [".ppt", ".pptx"],
|
|
319
|
+
extensions: [".ppt", ".pptx"], // 自家引擎能开二进制 .ppt 就一并申报
|
|
167
320
|
capabilities: {
|
|
168
|
-
editable:
|
|
169
|
-
importExtensions: [], exportFormats: [],
|
|
321
|
+
editable: true, aiOperable: false, collaborative: false,
|
|
322
|
+
importExtensions: [".ppt", ".pptx"], exportFormats: ["pptx"],
|
|
170
323
|
},
|
|
171
324
|
load: () => import("./MyOnlyOfficeSlideView"),
|
|
172
325
|
})
|
|
@@ -193,6 +346,6 @@ schema 不一致时一端产生的节点到另一端会被 y-prosemirror 当非
|
|
|
193
346
|
```bash
|
|
194
347
|
pnpm install
|
|
195
348
|
pnpm build # tsup + 样式编译
|
|
196
|
-
pnpm test #
|
|
349
|
+
pnpm test # 232 个用例
|
|
197
350
|
pnpm typecheck
|
|
198
351
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"PptxView-ZH4FZKNO.js"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"SheetView-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"SheetView-SDBRSTVS.js"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/xlsx/cell.ts
|
|
2
|
+
function numericKeys(o) {
|
|
3
|
+
if (!o) return [];
|
|
4
|
+
return Object.keys(o).map(Number).filter((n) => Number.isInteger(n) && n >= 0).sort((a, b) => a - b);
|
|
5
|
+
}
|
|
6
|
+
function matrixRow(matrix, row) {
|
|
7
|
+
let r = matrix[row];
|
|
8
|
+
if (!r) {
|
|
9
|
+
r = {};
|
|
10
|
+
matrix[row] = r;
|
|
11
|
+
}
|
|
12
|
+
return r;
|
|
13
|
+
}
|
|
14
|
+
function richTextToPlain(cell) {
|
|
15
|
+
const stream = cell.p?.body?.dataStream;
|
|
16
|
+
if (typeof stream !== "string") return void 0;
|
|
17
|
+
return stream.replace(/\r\n$/, "").replace(/\r/g, "\n");
|
|
18
|
+
}
|
|
19
|
+
function looksLikeDateFormat(pattern) {
|
|
20
|
+
if (!pattern) return false;
|
|
21
|
+
const bare = pattern.replace(/"[^"]*"/g, "").replace(/\\./g, "");
|
|
22
|
+
return /[yYdD]/.test(bare) || /h/i.test(bare) || /\[\$-/.test(pattern);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { looksLikeDateFormat, matrixRow, numericKeys, richTextToPlain };
|
|
26
|
+
//# sourceMappingURL=chunk-5HMKRNIH.js.map
|
|
27
|
+
//# sourceMappingURL=chunk-5HMKRNIH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/xlsx/cell.ts"],"names":[],"mappings":";AAWO,SAAS,YAAY,CAAA,EAAkD;AAC5E,EAAA,IAAI,CAAC,CAAA,EAAG,OAAO,EAAC;AAChB,EAAA,OAAO,MAAA,CAAO,KAAK,CAAC,CAAA,CACjB,IAAI,MAAM,CAAA,CACV,MAAA,CAAO,CAAC,CAAA,KAAM,MAAA,CAAO,UAAU,CAAC,CAAA,IAAK,KAAK,CAAC,CAAA,CAC3C,KAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AACzB;AAGO,SAAS,SAAA,CAAa,QAAmB,GAAA,EAAgC;AAC9E,EAAA,IAAI,CAAA,GAAI,OAAO,GAAG,CAAA;AAClB,EAAA,IAAI,CAAC,CAAA,EAAG;AACN,IAAA,CAAA,GAAI,EAAC;AACL,IAAA,MAAA,CAAO,GAAG,CAAA,GAAI,CAAA;AAAA,EAChB;AACA,EAAA,OAAO,CAAA;AACT;AAGO,SAAS,gBAAgB,IAAA,EAAoC;AAClE,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,CAAA,EAAG,IAAA,EAAM,UAAA;AAC7B,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,EAAU,OAAO,MAAA;AACvC,EAAA,OAAO,OAAO,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAA,CAAE,OAAA,CAAQ,OAAO,IAAI,CAAA;AACxD;AASO,SAAS,oBAAoB,OAAA,EAA6C;AAC/E,EAAA,IAAI,CAAC,SAAS,OAAO,KAAA;AAErB,EAAA,MAAM,IAAA,GAAO,QAAQ,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC/D,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,IAAK,IAAA,CAAK,KAAK,IAAI,CAAA,IAAK,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACvE","file":"chunk-5HMKRNIH.js","sourcesContent":["/** 读写 Univer 快照时 xlsx 与 csv 两条路径共用的小工具。 */\n\nimport type { CellData, Matrix } from \"./types.ts\"\n\n/**\n * 稀疏矩阵的行号/列号。\n *\n * Univer 的 `cellData` 是 `{ [row]: { [col]: cell } }` 的普通对象,键是字符串。\n * 直接 `Object.keys()` 会得到 `[\"10\", \"2\"]` 这种字典序,必须转数字再排 ——\n * 否则导出的行序就是乱的。\n */\nexport function numericKeys(o: Record<number, unknown> | undefined): number[] {\n if (!o) return []\n return Object.keys(o)\n .map(Number)\n .filter((n) => Number.isInteger(n) && n >= 0)\n .sort((a, b) => a - b)\n}\n\n/** 取稀疏矩阵的一行,不存在则建。导入侧写 cellData 用 */\nexport function matrixRow<T>(matrix: Matrix<T>, row: number): Record<number, T> {\n let r = matrix[row]\n if (!r) {\n r = {}\n matrix[row] = r\n }\n return r\n}\n\n/** 富文本只取纯文本。Univer 的 dataStream 用 \\r 断段、结尾还带一个 \\r\\n */\nexport function richTextToPlain(cell: CellData): string | undefined {\n const stream = cell.p?.body?.dataStream\n if (typeof stream !== \"string\") return undefined\n return stream.replace(/\\r\\n$/, \"\").replace(/\\r/g, \"\\n\")\n}\n\n/**\n * 这个单元格的数字格式是不是日期/时间。\n *\n * 只做**粗判**,用来给 csv 提示「这里会导出成序列号」,不用来做转换 ——\n * Excel 的格式码有转义(`\\d`、`\"年\"`)、有区域前缀(`[$-409]`)、\n * 还有和 `mm` 同形的分钟,真解析是个坑。猜错日期比原样给出序列号更糟。\n */\nexport function looksLikeDateFormat(pattern: string | undefined | null): boolean {\n if (!pattern) return false\n // 去掉引号里的字面量和反斜杠转义,剩下的才是真正的格式符\n const bare = pattern.replace(/\"[^\"]*\"/g, \"\").replace(/\\\\./g, \"\")\n return /[yYdD]/.test(bare) || /h/i.test(bare) || /\\[\\$-/.test(pattern)\n}\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { unzipSync, zipSync } from 'fflate';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/views/PptxView.tsx
|
|
6
|
+
var CONTENT_TYPES = "[Content_Types].xml";
|
|
7
|
+
var MASTER_ENTRY = /^ppt\/slideMasters\/slideMaster(\d+)\.xml$/;
|
|
8
|
+
var MASTER_DECL = /PartName="\/ppt\/slideMasters\/slideMaster(\d+)\.xml"/g;
|
|
9
|
+
function repairPptx(input) {
|
|
10
|
+
let files;
|
|
11
|
+
try {
|
|
12
|
+
files = unzipSync(input);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return { bytes: input, removed: 0, skipped: `不是有效的 zip:${e.message}` };
|
|
15
|
+
}
|
|
16
|
+
const contentTypes = files[CONTENT_TYPES];
|
|
17
|
+
if (!contentTypes) return { bytes: input, removed: 0, skipped: `缺少 ${CONTENT_TYPES}` };
|
|
18
|
+
const actual = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const name of Object.keys(files)) {
|
|
20
|
+
const m = MASTER_ENTRY.exec(name);
|
|
21
|
+
if (m) actual.add(m[1]);
|
|
22
|
+
}
|
|
23
|
+
const xml = new TextDecoder().decode(contentTypes);
|
|
24
|
+
const ghosts = [...xml.matchAll(MASTER_DECL)].map((m) => m[1]).filter((n) => !actual.has(n));
|
|
25
|
+
if (!ghosts.length) return { bytes: input, removed: 0 };
|
|
26
|
+
let patched = xml;
|
|
27
|
+
for (const n of new Set(ghosts)) {
|
|
28
|
+
patched = patched.replace(
|
|
29
|
+
new RegExp(`<Override[^>]+PartName="/ppt/slideMasters/slideMaster${n}\\.xml"[^>]*>\\s*`, "g"),
|
|
30
|
+
""
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (!patched.trim()) return { bytes: input, removed: 0, skipped: "替换后内容为空" };
|
|
34
|
+
files[CONTENT_TYPES] = new TextEncoder().encode(patched);
|
|
35
|
+
try {
|
|
36
|
+
return { bytes: zipSync(files, { level: 6 }), removed: new Set(ghosts).size };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return { bytes: input, removed: 0, skipped: `重新打包失败:${e.message}` };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var BASE_WIDTH = 960;
|
|
42
|
+
function PptxView({
|
|
43
|
+
source,
|
|
44
|
+
content,
|
|
45
|
+
className,
|
|
46
|
+
onReady,
|
|
47
|
+
onError
|
|
48
|
+
}) {
|
|
49
|
+
const hostRef = useRef(null);
|
|
50
|
+
const wrapperRef = useRef(null);
|
|
51
|
+
const [state, setState] = useState("loading");
|
|
52
|
+
const [slides, setSlides] = useState(0);
|
|
53
|
+
const latest = useRef({ onReady, onError });
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
latest.current = { onReady, onError };
|
|
56
|
+
});
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
const host = hostRef.current;
|
|
60
|
+
if (!host || !source && !content) return;
|
|
61
|
+
let previewer = null;
|
|
62
|
+
setState("loading");
|
|
63
|
+
setSlides(0);
|
|
64
|
+
(async () => {
|
|
65
|
+
const { onReady: ready, onError: fail } = latest.current;
|
|
66
|
+
try {
|
|
67
|
+
const [{ init }, bytes] = await Promise.all([
|
|
68
|
+
import('pptx-preview'),
|
|
69
|
+
toBytes(source, content)
|
|
70
|
+
]);
|
|
71
|
+
if (cancelled) return;
|
|
72
|
+
const { bytes: fixed, removed, skipped } = repairPptx(bytes);
|
|
73
|
+
if (skipped) console.warn(`[PptxView] 跳过修复:${skipped}`);
|
|
74
|
+
else if (removed) console.info(`[PptxView] 已移除 ${removed} 条无效的 slideMaster 声明`);
|
|
75
|
+
host.innerHTML = "";
|
|
76
|
+
previewer = init(host, { width: BASE_WIDTH });
|
|
77
|
+
await previewer.preview(
|
|
78
|
+
toArrayBuffer(fixed)
|
|
79
|
+
);
|
|
80
|
+
if (cancelled) return;
|
|
81
|
+
const count = previewer.slideCount ?? 0;
|
|
82
|
+
setSlides(count);
|
|
83
|
+
if (!count) {
|
|
84
|
+
setState("error");
|
|
85
|
+
fail?.(new Error("演示文稿解析失败:没有可渲染的幻灯片"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setState("ready");
|
|
89
|
+
ready?.(previewer);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (cancelled) return;
|
|
92
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
93
|
+
setState("error");
|
|
94
|
+
fail?.(
|
|
95
|
+
new Error(
|
|
96
|
+
`演示文稿预览失败:${err.message}。请确认已安装可选依赖 pptx-preview(pnpm add pptx-preview)`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
return () => {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
previewer?.destroy?.();
|
|
104
|
+
if (host) host.innerHTML = "";
|
|
105
|
+
};
|
|
106
|
+
}, [source, content]);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const wrapper = wrapperRef.current;
|
|
109
|
+
const host = hostRef.current;
|
|
110
|
+
if (!wrapper || !host) return;
|
|
111
|
+
const update = () => {
|
|
112
|
+
const width = wrapper.clientWidth;
|
|
113
|
+
host.style.zoom = width && width < BASE_WIDTH ? String(width / BASE_WIDTH) : "";
|
|
114
|
+
};
|
|
115
|
+
update();
|
|
116
|
+
const observer = new ResizeObserver(update);
|
|
117
|
+
observer.observe(wrapper);
|
|
118
|
+
return () => observer.disconnect();
|
|
119
|
+
}, []);
|
|
120
|
+
return /* @__PURE__ */ jsx(
|
|
121
|
+
"div",
|
|
122
|
+
{
|
|
123
|
+
ref: wrapperRef,
|
|
124
|
+
className,
|
|
125
|
+
"data-pptx-state": state,
|
|
126
|
+
"data-pptx-slides": slides,
|
|
127
|
+
style: { width: "100%", height: "100%", overflow: "auto" },
|
|
128
|
+
children: /* @__PURE__ */ jsx("div", { ref: hostRef, style: { width: BASE_WIDTH } })
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
async function toBytes(source, content) {
|
|
133
|
+
if (content instanceof Uint8Array) return content;
|
|
134
|
+
if (content instanceof ArrayBuffer) return new Uint8Array(content);
|
|
135
|
+
if (source instanceof Blob) return new Uint8Array(await source.arrayBuffer());
|
|
136
|
+
if (typeof source === "string") {
|
|
137
|
+
const res = await fetch(source);
|
|
138
|
+
if (!res.ok) throw new Error(`拉取失败 HTTP ${res.status}`);
|
|
139
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
140
|
+
}
|
|
141
|
+
throw new Error("需要 source(URL / File / Blob)或 content(ArrayBuffer)");
|
|
142
|
+
}
|
|
143
|
+
function toArrayBuffer(bytes) {
|
|
144
|
+
return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes.buffer : bytes.slice().buffer;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { PptxView, repairPptx };
|
|
148
|
+
//# sourceMappingURL=chunk-5XG4YMAB.js.map
|
|
149
|
+
//# sourceMappingURL=chunk-5XG4YMAB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pptx-repair.ts","../src/views/PptxView.tsx"],"names":[],"mappings":"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","file":"chunk-5XG4YMAB.js","sourcesContent":["/**\n * pptx 的「幽灵 slideMaster」修复。\n *\n * 很多 pptx 的 `[Content_Types].xml` 里声明了 zip 中**并不存在**的\n * `ppt/slideMasters/slideMasterN.xml`。PowerPoint 自己不介意,但 `pptx-preview`\n * 会因为解析不到这些部件而整份失败 —— 表现是 `slideCount === 0`、面板一片空白,\n * 不抛错、不报警。\n *\n * 这不是边缘情况:在 17 份真实文件上实测,**12 份中招**(AI 生成的和\n * PowerPoint 16 存出来的都有)。裸接这个库只有 5/17 能打开,加上本修复后 17/17。\n *\n * 做法与腾讯 WorkBuddy 内置 pptx 预览里的 `checkPPTXFile` 一致:删掉指向缺失\n * 部件的 `<Override>` 再重新打包。只处理 slideMaster —— 其它部件缺失时该库\n * 能自己降级,动它反而可能改坏。\n */\n\nimport { unzipSync, zipSync } from \"fflate\"\n\nconst CONTENT_TYPES = \"[Content_Types].xml\"\nconst MASTER_ENTRY = /^ppt\\/slideMasters\\/slideMaster(\\d+)\\.xml$/\nconst MASTER_DECL = /PartName=\"\\/ppt\\/slideMasters\\/slideMaster(\\d+)\\.xml\"/g\n\nexport interface RepairPptxResult {\n /** 修好的字节;没问题时**原样返回入参**,不做无谓的解压重打包 */\n bytes: Uint8Array\n /** 删掉了几条幽灵声明。0 表示这份文件本来就是好的 */\n removed: number\n /** 修复过程中放弃的原因(文件不是 zip、没有 [Content_Types].xml 等) */\n skipped?: string\n}\n\n/**\n * @param input pptx 原始字节\n * @returns 可交给 `pptx-preview` 的字节。**任何异常都退回原字节** ——\n * 修不了也不该让预览失败,让引擎自己去报它的错。\n */\nexport function repairPptx(input: Uint8Array): RepairPptxResult {\n let files: Record<string, Uint8Array>\n try {\n files = unzipSync(input)\n } catch (e) {\n return { bytes: input, removed: 0, skipped: `不是有效的 zip:${(e as Error).message}` }\n }\n\n const contentTypes = files[CONTENT_TYPES]\n if (!contentTypes) return { bytes: input, removed: 0, skipped: `缺少 ${CONTENT_TYPES}` }\n\n const actual = new Set<string>()\n for (const name of Object.keys(files)) {\n const m = MASTER_ENTRY.exec(name)\n if (m) actual.add(m[1] as string)\n }\n\n const xml = new TextDecoder().decode(contentTypes)\n const ghosts = [...xml.matchAll(MASTER_DECL)]\n .map((m) => m[1] as string)\n .filter((n) => !actual.has(n))\n if (!ghosts.length) return { bytes: input, removed: 0 }\n\n let patched = xml\n for (const n of new Set(ghosts)) {\n patched = patched.replace(\n new RegExp(`<Override[^>]+PartName=\"/ppt/slideMasters/slideMaster${n}\\\\.xml\"[^>]*>\\\\s*`, \"g\"),\n \"\",\n )\n }\n // 替换后为空说明正则吃掉了不该吃的,宁可不修\n if (!patched.trim()) return { bytes: input, removed: 0, skipped: \"替换后内容为空\" }\n\n files[CONTENT_TYPES] = new TextEncoder().encode(patched)\n try {\n return { bytes: zipSync(files, { level: 6 }), removed: new Set(ghosts).size }\n } catch (e) {\n return { bytes: input, removed: 0, skipped: `重新打包失败:${(e as Error).message}` }\n }\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport { repairPptx } from \"../pptx-repair\"\nimport type { DocumentViewProps } from \"../registry\"\n\n/**\n * pptx 通道:`pptx-preview` 只读预览。\n *\n * **只做「看清这页讲什么」,不是所见即所得。** 已知差距:\n * - **图表渲染不出数据** —— 轴画得出来,柱子/折线是空的,系列名与标题退化成默认值。\n * 该库打包了 ECharts 却没把 `c:cat` / `c:val` 接上去\n * - 原字体缺失时替换成系统字体,观感和原稿不一致\n * - 内部偶有 SVG 路径算出 NaN 的报错,个别形状会画不出来\n *\n * 编辑不做:开源世界没有可用的前端 pptx 编辑引擎(Univer Slides 官方标注\n * 「请勿用于生产环境」),真要做只有 OnlyOffice / Collabora 那一档\n * (AGPL,需法务口径 + 独立部署)。宿主可以用 `registerFormat` 覆盖本通道。\n *\n * 引擎选型与「幽灵 slideMaster」修复都对齐腾讯 WorkBuddy 的内置 pptx 预览。\n */\n\n/** 该库以此宽度布局,缩放靠 CSS zoom。和 WorkBuddy 取值一致 */\nconst BASE_WIDTH = 960\n\nexport default function PptxView({\n source,\n content,\n className,\n onReady,\n onError,\n}: DocumentViewProps) {\n const hostRef = useRef<HTMLDivElement>(null)\n const wrapperRef = useRef<HTMLDivElement>(null)\n const [state, setState] = useState<\"loading\" | \"ready\" | \"error\">(\"loading\")\n const [slides, setSlides] = useState(0)\n\n // 同 SheetView:回调多是内联箭头函数,进依赖数组会让父组件每渲染一次就重解析一遍\n const latest = useRef({ onReady, onError })\n useEffect(() => {\n latest.current = { onReady, onError }\n })\n\n useEffect(() => {\n let cancelled = false\n const host = hostRef.current\n if (!host || (!source && !content)) return\n let previewer: { destroy?: () => void } | null = null\n\n setState(\"loading\")\n setSlides(0)\n ;(async () => {\n const { onReady: ready, onError: fail } = latest.current\n try {\n // 动态 import:这个库连着 ECharts 一起 gzip 后 400KB+,\n // 不该因为「包里支持 pptx」就让所有人付这笔钱\n const [{ init }, bytes] = await Promise.all([\n import(\"pptx-preview\"),\n toBytes(source, content),\n ])\n if (cancelled) return\n\n // 先修再喂。不修的话真实文件里大半会静默变成 0 页空白\n const { bytes: fixed, removed, skipped } = repairPptx(bytes)\n if (skipped) console.warn(`[PptxView] 跳过修复:${skipped}`)\n else if (removed) console.info(`[PptxView] 已移除 ${removed} 条无效的 slideMaster 声明`)\n\n host.innerHTML = \"\"\n // 只传 width:不传 mode 时该库按整列堆叠渲染,正是预览面板要的\n previewer = (init as (el: HTMLElement, o: object) => never)(host, { width: BASE_WIDTH })\n await (previewer as unknown as { preview: (b: ArrayBuffer) => Promise<unknown> }).preview(\n toArrayBuffer(fixed),\n )\n if (cancelled) return\n\n const count = (previewer as unknown as { slideCount?: number }).slideCount ?? 0\n setSlides(count)\n if (!count) {\n // 该库解析失败时不抛错,只是什么都不画 —— 不显式判空就会是一片白板\n setState(\"error\")\n fail?.(new Error(\"演示文稿解析失败:没有可渲染的幻灯片\"))\n return\n }\n setState(\"ready\")\n ready?.(previewer)\n } catch (e) {\n if (cancelled) return\n const err = e instanceof Error ? e : new Error(String(e))\n setState(\"error\")\n fail?.(\n new Error(\n `演示文稿预览失败:${err.message}。` +\n `请确认已安装可选依赖 pptx-preview(pnpm add pptx-preview)`,\n ),\n )\n }\n })()\n\n return () => {\n cancelled = true\n previewer?.destroy?.()\n if (host) host.innerHTML = \"\"\n }\n }, [source, content])\n\n // 容器比 960 窄时整体缩放。用 zoom 而不是 transform:\n // transform 不改变布局盒,缩完外层还会留着原高度的空白\n useEffect(() => {\n const wrapper = wrapperRef.current\n const host = hostRef.current\n if (!wrapper || !host) return\n const update = () => {\n const width = wrapper.clientWidth\n host.style.zoom = width && width < BASE_WIDTH ? String(width / BASE_WIDTH) : \"\"\n }\n update()\n const observer = new ResizeObserver(update)\n observer.observe(wrapper)\n return () => observer.disconnect()\n }, [])\n\n return (\n <div\n ref={wrapperRef}\n className={className}\n data-pptx-state={state}\n data-pptx-slides={slides}\n style={{ width: \"100%\", height: \"100%\", overflow: \"auto\" }}\n >\n <div ref={hostRef} style={{ width: BASE_WIDTH }} />\n </div>\n )\n}\n\nasync function toBytes(\n source: DocumentViewProps[\"source\"],\n content: DocumentViewProps[\"content\"],\n): Promise<Uint8Array> {\n if (content instanceof Uint8Array) return content\n if (content instanceof ArrayBuffer) return new Uint8Array(content)\n if (source instanceof Blob) return new Uint8Array(await source.arrayBuffer())\n if (typeof source === \"string\") {\n const res = await fetch(source)\n if (!res.ok) throw new Error(`拉取失败 HTTP ${res.status}`)\n return new Uint8Array(await res.arrayBuffer())\n }\n throw new Error(\"需要 source(URL / File / Blob)或 content(ArrayBuffer)\")\n}\n\n/** fflate 可能返回带 offset 的视图,直接取 .buffer 会把整块底层内存交出去 */\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength\n ? (bytes.buffer as ArrayBuffer)\n : (bytes.slice().buffer as ArrayBuffer)\n}\n"]}
|