@4399ywkf/editor 0.6.0 → 0.8.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.
Files changed (61) hide show
  1. package/README.md +161 -2
  2. package/dist/{DocView-XQMJUUVK.js → DocView-WEKRSKRU.js} +6 -4
  3. package/dist/DocView-WEKRSKRU.js.map +1 -0
  4. package/dist/SheetView-SDBRSTVS.js +3 -0
  5. package/dist/{SheetView-DSADIZNS.js.map → SheetView-SDBRSTVS.js.map} +1 -1
  6. package/dist/chunk-5HMKRNIH.js +27 -0
  7. package/dist/chunk-5HMKRNIH.js.map +1 -0
  8. package/dist/chunk-DGUNVCOM.js +711 -0
  9. package/dist/chunk-DGUNVCOM.js.map +1 -0
  10. package/dist/chunk-HPSPOM2O.js +128 -0
  11. package/dist/chunk-HPSPOM2O.js.map +1 -0
  12. package/dist/chunk-KRP5V7VF.js +130 -0
  13. package/dist/chunk-KRP5V7VF.js.map +1 -0
  14. package/dist/chunk-SBTKDJAL.js +107 -0
  15. package/dist/chunk-SBTKDJAL.js.map +1 -0
  16. package/dist/{chunk-IUDLMN53.js → chunk-SGLNT6Z2.js} +2416 -176
  17. package/dist/chunk-SGLNT6Z2.js.map +1 -0
  18. package/dist/{chunk-L6ZR5HHW.js → chunk-SYCK6Y5F.js} +15 -10
  19. package/dist/chunk-SYCK6Y5F.js.map +1 -0
  20. package/dist/chunk-WF7PIIFR.js +21 -0
  21. package/dist/chunk-WF7PIIFR.js.map +1 -0
  22. package/dist/chunk-ZY5KLA7T.js +258 -0
  23. package/dist/chunk-ZY5KLA7T.js.map +1 -0
  24. package/dist/code-types-Dqmit3xx.d.ts +65 -0
  25. package/dist/{types-C37MAt8o.d.ts → csv-parse-D0zTBvDB.d.ts} +39 -1
  26. package/dist/csv.d.ts +6 -3
  27. package/dist/csv.js +6 -125
  28. package/dist/csv.js.map +1 -1
  29. package/dist/doc-runtime.d.ts +1 -1
  30. package/dist/doc-runtime.js +7 -1
  31. package/dist/doc-runtime.js.map +1 -1
  32. package/dist/doc.d.ts +113 -6
  33. package/dist/doc.js +2 -1
  34. package/dist/doc.js.map +1 -1
  35. package/dist/docx.d.ts +15 -0
  36. package/dist/docx.js +116 -8
  37. package/dist/docx.js.map +1 -1
  38. package/dist/import-GVZDLWA2.js +6 -0
  39. package/dist/import-GVZDLWA2.js.map +1 -0
  40. package/dist/index.d.ts +4 -2
  41. package/dist/index.js +7 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/pdf.d.ts +3 -1
  44. package/dist/pptx.d.ts +3 -1
  45. package/dist/{registry-BBL8yeIT.d.ts → registry-CYKivRDh.d.ts} +13 -1
  46. package/dist/sheet-runtime.d.ts +212 -0
  47. package/dist/sheet-runtime.js +416 -0
  48. package/dist/sheet-runtime.js.map +1 -0
  49. package/dist/sheet.d.ts +9 -5
  50. package/dist/sheet.js +1 -1
  51. package/dist/style.css +421 -111
  52. package/dist/xlsx.d.ts +118 -7
  53. package/dist/xlsx.js +7 -78
  54. package/dist/xlsx.js.map +1 -1
  55. package/package.json +11 -5
  56. package/dist/DocView-XQMJUUVK.js.map +0 -1
  57. package/dist/SheetView-DSADIZNS.js +0 -3
  58. package/dist/chunk-32WQX3NR.js +0 -37
  59. package/dist/chunk-32WQX3NR.js.map +0 -1
  60. package/dist/chunk-IUDLMN53.js.map +0 -1
  61. package/dist/chunk-L6ZR5HHW.js.map +0 -1
package/README.md CHANGED
@@ -24,6 +24,12 @@ import "@4399ywkf/editor/styles"
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
35
  主入口只有 **3KB**(格式路由),tiptap 的 698KB 在独立 chunk 里,Univer / pdf.js / pptx-preview 都是可选 peer。
@@ -36,11 +42,13 @@ 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"
40
46
  import { downloadCsv } from "@4399ywkf/editor/csv"
41
47
  import { SheetView } from "@4399ywkf/editor/sheet"
42
48
  import { PdfView } from "@4399ywkf/editor/pdf"
43
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" // 同上,表格版
44
52
  import "@4399ywkf/editor/styles"
45
53
  ```
46
54
 
@@ -197,6 +205,131 @@ if (warnings.length) toast(warnings.join("\n"))
197
205
  「序列号 + 数字格式」,csv 里会写成 `45678`。格式码有转义、区域前缀、还有和 `mm`
198
206
  同形的分钟,猜错日期比原样给出序列号更糟,所以只警告不转换。
199
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
+
252
+ ## 代码块与代码分组(Shiki)
253
+
254
+ 代码块用 [Shiki](https://shiki.style) 高亮:默认 `github-light` / `github-dark` 随 `.dark`
255
+ 切换,语言按需加载,不需要任何配置。高亮通过 ProseMirror Decoration 着色,正文仍由
256
+ ProseMirror 管理 —— 不改文档、不进撤销历史、不广播给协作者。
257
+
258
+ **代码分组**:在正文里输入或粘贴 `::: code-group` 语法,得到一个可切换标签、可编辑、
259
+ 可复制的分组;也可以用 `contentFormat="markdown"` 直接载入:
260
+
261
+ ```tsx
262
+ import { NotionEditor } from "@4399ywkf/editor/doc"
263
+
264
+ const codeGroupMarkdown = `
265
+ ::: code-group
266
+ \`\`\`bash [curl]
267
+ curl https://api.example.com/v1/chat
268
+ \`\`\`
269
+
270
+ \`\`\`python [python]
271
+ import requests
272
+ \`\`\`
273
+ :::
274
+ `
275
+
276
+ <NotionEditor
277
+ room="api-example"
278
+ contentFormat="markdown"
279
+ content={codeGroupMarkdown}
280
+ onEditorReady={(editor) => save(editor.getJSON())}
281
+ />
282
+ ```
283
+
284
+ - `contentFormat` 可选 `"json" | "html" | "markdown"`。**不传时沿用旧规则:对象按 JSON、
285
+ 字符串按 HTML**。`"json"` 只接受对象,不会去解析 JSON 文本;格式与内容类型不匹配时
286
+ console.error 一条 `[editor]` 信息,当前文档不动。`ArticleViewer` 与 `DocumentView`
287
+ 的 doc 通道接受同样的 `contentFormat`。
288
+ - fence 的 info 按 `语言 [标签]` 解析:`bash [curl]` → 语言 `bash`、标签 `curl`;只写
289
+ `python` 则标签栏显示语言名。标签允许空格与 `\[` `\]` `\\` 转义。
290
+ - 组内只允许完整的 fenced code 与空行;没闭合或混进正文的组保持普通文本,可继续编辑。
291
+ 代码里的 `:::` 不会关闭组。
292
+ - 输出:`editor.getJSON()`(规范结构,`codeGroup` 只含 `codeBlock+`)、`editor.getHTML()`
293
+ (语义 HTML:`<div data-code-group><pre data-language data-label>…`)、
294
+ `getEditorMarkdown(editor)`(`::: code-group` 语法;等价于 `editor.getMarkdown()`,
295
+ 但不需要引 `@tiptap/markdown` 的类型扩充)。`renderHighlightedHTML(editor)` 异步导出
296
+ 调用时刻文档快照的静态 Shiki HTML,所有标签的代码都在、不嵌脚本。
297
+ - 活动标签是 NodeView 本地状态,不写进 JSON / Yjs。本期不提供标签的新增 / 删除 / 排序 /
298
+ 重命名,也不会执行任何代码。
299
+ - `codeBlock` prop 可覆盖主题 / 别名 / 额外语言:`{ shikiTheme, languageAliases, languages }`。
300
+
301
+ ## 折叠块、提示框与 API 参数
302
+
303
+ 三种可折叠块容器,Markdown 都是 VitePress 风格的 `:::` 容器(前两种与 VitePress 原样兼容):
304
+
305
+ ```md
306
+ ::: details 点击查看
307
+ 藏起来的内容
308
+ :::
309
+
310
+ ::: warning 注意
311
+ 不可与 instructions 字段一起使用。
312
+ :::
313
+
314
+ :::: param caching `object`
315
+ 上下文缓存
316
+
317
+ 是否为本次请求开启上下文缓存。
318
+
319
+ ::: param prefix `boolean` 默认 `false`
320
+ 仅写缓存
321
+ :::
322
+ ::::
323
+ ```
324
+
325
+ - 提示框类型:`info` / `tip` / `warning` / `danger`,冒号后的文字是标题(可空,空时显示 信息 / 提示 / 注意 / 危险)。
326
+ - `param` 信息串:第一个词是参数名,反引号里是类型(允许 `string / object[]`),`必选`(或 `required`),`默认 值`(或 `default 值`,值带空格用反引号)。正文第一段是折叠行右边那句短描述,其余段落展开后才显示。
327
+ - 嵌套:外层用更多冒号(`::::`)。代码 fence 里的 `:::` 不会关闭容器;没闭合的容器保持普通文本。
328
+ - 展开 / 收起不进文档:读者打开时全部收起,编辑态光标进入隐藏正文时自动展开。
329
+ - 编辑态头部字段(标题、参数名、类型、必选、默认值、短描述)就地改,失焦或回车提交(一步撤销),Esc 还原;slash 菜单有"折叠块 / 提示框 / 参数"三项;在段落里输入 `::: warning 注意` 回车也能当场生成。
330
+ - `getHTML()` 输出原生 `<details>`(无脚本也能折叠)与 `<div data-callout>`;docx 按顺序摊平为"标题 + 正文",嵌套缩进递增;LiteXML 里是 `<details>` / `<callout>` / `<param>`。
331
+ - 载入:`<NotionEditor contentFormat="markdown" content={markdown} />`。OpenAPI / JSON Schema 导入是后续子项目,参数节点的 JSON 结构就是它的目标形态。
332
+
200
333
  ## docx 导入导出
201
334
 
202
335
  API 形态对齐 `@tiptap-pro/extension-import-docx` / `-export-docx`,但**在本地解析**,
@@ -223,6 +356,32 @@ editor.chain().exportDocx({ onCompleteExport: (blob) => save(blob) }).run()
223
356
 
224
357
  已知缺口:`fldChar` 复杂域、修订痕迹、页眉页脚 / 脚注、超链接导出未建外部关系。
225
358
 
359
+ ## AI 操作面
360
+
361
+ 要让 AI 改**用户此刻正在看的那个实例**(而不是复制一份文本出去改),
362
+ 挂对应的 runtime 即可。两层都只认编辑器实例,**不知道 MCP / HTTP 存在**:
363
+
364
+ ```tsx
365
+ import { DocRuntime } from "@4399ywkf/editor/doc-runtime"
366
+ import { SheetRuntime } from "@4399ywkf/editor/sheet-runtime"
367
+
368
+ const doc = useMemo(() => new DocRuntime(), [])
369
+ const sheet = useMemo(() => new SheetRuntime(), [])
370
+
371
+ <NotionEditor onEditorReady={(editor) => doc.setEditor(editor)} />
372
+ <DocumentView format="sheet" onReady={(api) => sheet.setUniverAPI(api)} />
373
+
374
+ await doc.call("doc_replace_text", { ... }) // 两个 runtime 的 call 是同构的
375
+ await sheet.call("sheet_read", { range: "A1:D20" })
376
+ ```
377
+
378
+ - `doc-runtime`:LiteXML(对 ProseMirror JSON 的有损压缩,省 1.55x 以上 token)+ 8 类操作。
379
+ - `sheet-runtime`:读走快照(CSV 文本,比 JSON 单元格矩阵省几倍 token),
380
+ 写走 Univer facade 的 command 体系(这样才有撤销栈和协同广播)。
381
+
382
+ 工具定义(name / JSON Schema / description)和传输(浏览器桥 / hub / stdio MCP)
383
+ 都在 [`@4399ywkf/editor-mcp`](../editor-mcp),本包一行传输代码都没有。
384
+
226
385
  ## 注册自有格式
227
386
 
228
387
  内置通道覆盖不了、或保真度不够时,宿主可以用同名 `registerFormat` 直接覆盖。
@@ -268,6 +427,6 @@ schema 不一致时一端产生的节点到另一端会被 y-prosemirror 当非
268
427
  ```bash
269
428
  pnpm install
270
429
  pnpm build # tsup + 样式编译
271
- pnpm test # 203 个用例
430
+ pnpm test # 232 个用例
272
431
  pnpm typecheck
273
432
  ```
@@ -1,13 +1,15 @@
1
- import { EditorConfigProvider, NotionEditor } from './chunk-IUDLMN53.js';
1
+ import { EditorConfigProvider, NotionEditor } from './chunk-SGLNT6Z2.js';
2
2
  import './chunk-QMYT5WXO.js';
3
+ import './chunk-ZY5KLA7T.js';
3
4
  import { jsx } from 'react/jsx-runtime';
4
5
 
5
- function DocView({ content, readOnly, room, className, onReady }) {
6
+ function DocView({ content, contentFormat, readOnly, room, className, onReady }) {
6
7
  return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(EditorConfigProvider, { config: {}, children: /* @__PURE__ */ jsx(
7
8
  NotionEditor,
8
9
  {
9
10
  room: room ?? "default",
10
11
  content,
12
+ contentFormat,
11
13
  readOnly,
12
14
  onEditorReady: (editor) => onReady?.(editor)
13
15
  }
@@ -15,5 +17,5 @@ function DocView({ content, readOnly, room, className, onReady }) {
15
17
  }
16
18
 
17
19
  export { DocView as default };
18
- //# sourceMappingURL=DocView-XQMJUUVK.js.map
19
- //# sourceMappingURL=DocView-XQMJUUVK.js.map
20
+ //# sourceMappingURL=DocView-WEKRSKRU.js.map
21
+ //# sourceMappingURL=DocView-WEKRSKRU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/views/DocView.tsx"],"names":[],"mappings":";;;;;AAMe,SAAR,OAAA,CAAyB,EAAE,OAAA,EAAS,aAAA,EAAe,UAAU,IAAA,EAAM,SAAA,EAAW,SAAQ,EAAsB;AACjH,EAAA,2BACG,KAAA,EAAA,EAAI,SAAA,EACH,8BAAC,oBAAA,EAAA,EAAqB,MAAA,EAAQ,EAAC,EAC7B,QAAA,kBAAA,GAAA;AAAA,IAAC,YAAA;AAAA,IAAA;AAAA,MACC,MAAM,IAAA,IAAQ,SAAA;AAAA,MACd,OAAA;AAAA,MACA,aAAA;AAAA,MACA,QAAA;AAAA,MACA,aAAA,EAAe,CAAC,MAAA,KAAmB,OAAA,GAAU,MAAM;AAAA;AAAA,KAEvD,CAAA,EACF,CAAA;AAEJ","file":"DocView-WEKRSKRU.js","sourcesContent":["import type { Editor } from \"@tiptap/react\"\nimport { EditorConfigProvider } from \"../contexts/editor-config-context\"\nimport { NotionEditor } from \"../components/tiptap-templates/notion-like/notion-like-editor\"\nimport type { DocumentViewProps } from \"../registry\"\n\n/** doc 通道:云文档与本地 doc 是同一套 tiptap,不做区分 */\nexport default function DocView({ content, contentFormat, readOnly, room, className, onReady }: DocumentViewProps) {\n return (\n <div className={className}>\n <EditorConfigProvider config={{}}>\n <NotionEditor\n room={room ?? \"default\"}\n content={content as string}\n contentFormat={contentFormat}\n readOnly={readOnly}\n onEditorReady={(editor: Editor) => onReady?.(editor)}\n />\n </EditorConfigProvider>\n </div>\n )\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export { SheetView as default } from './chunk-SYCK6Y5F.js';
2
+ //# sourceMappingURL=SheetView-SDBRSTVS.js.map
3
+ //# sourceMappingURL=SheetView-SDBRSTVS.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"SheetView-DSADIZNS.js"}
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"]}