@casualoffice/sheets 0.9.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 (92) hide show
  1. package/LICENSE +200 -0
  2. package/dist/embed/embed-runtime.js +537 -0
  3. package/dist/embed/embed.html +29 -0
  4. package/dist/embed/parser.worker.js +48474 -0
  5. package/dist/embed.cjs +225 -0
  6. package/dist/embed.cjs.map +1 -0
  7. package/dist/embed.d.cts +100 -0
  8. package/dist/embed.d.ts +100 -0
  9. package/dist/embed.js +204 -0
  10. package/dist/embed.js.map +1 -0
  11. package/dist/index.cjs +1549 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +9 -0
  14. package/dist/index.d.ts +9 -0
  15. package/dist/index.js +1530 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/parser.worker.cjs +48469 -0
  18. package/dist/parser.worker.cjs.map +1 -0
  19. package/dist/parser.worker.js +48474 -0
  20. package/dist/parser.worker.js.map +1 -0
  21. package/dist/protocol--KyBQUjU.d.cts +171 -0
  22. package/dist/protocol-cEzy7S0i.d.ts +171 -0
  23. package/dist/sheets.cjs +677 -0
  24. package/dist/sheets.cjs.map +1 -0
  25. package/dist/sheets.d.cts +177 -0
  26. package/dist/sheets.d.ts +177 -0
  27. package/dist/sheets.js +658 -0
  28. package/dist/sheets.js.map +1 -0
  29. package/dist/signing.cjs +706 -0
  30. package/dist/signing.cjs.map +1 -0
  31. package/dist/signing.d.cts +141 -0
  32. package/dist/signing.d.ts +141 -0
  33. package/dist/signing.js +683 -0
  34. package/dist/signing.js.map +1 -0
  35. package/dist/styles.cjs +10 -0
  36. package/dist/styles.cjs.map +1 -0
  37. package/dist/styles.d.cts +2 -0
  38. package/dist/styles.d.ts +2 -0
  39. package/dist/styles.js +8 -0
  40. package/dist/styles.js.map +1 -0
  41. package/dist/types-s_O0u6Cg.d.cts +90 -0
  42. package/dist/types-s_O0u6Cg.d.ts +90 -0
  43. package/dist/univer.cjs +220 -0
  44. package/dist/univer.cjs.map +1 -0
  45. package/dist/univer.d.cts +60 -0
  46. package/dist/univer.d.ts +60 -0
  47. package/dist/univer.js +187 -0
  48. package/dist/univer.js.map +1 -0
  49. package/dist/xlsx.cjs +3388 -0
  50. package/dist/xlsx.cjs.map +1 -0
  51. package/dist/xlsx.d.cts +383 -0
  52. package/dist/xlsx.d.ts +383 -0
  53. package/dist/xlsx.js +3383 -0
  54. package/dist/xlsx.js.map +1 -0
  55. package/package.json +293 -0
  56. package/src/embed/EmbedHostTransport.ts +226 -0
  57. package/src/embed/EmbedTransport.ts +323 -0
  58. package/src/embed/EmbedTransport.unit.test.ts +161 -0
  59. package/src/embed/index.ts +40 -0
  60. package/src/embed/protocol.ts +258 -0
  61. package/src/embed-runtime/embed.html +29 -0
  62. package/src/embed-runtime/index.tsx +440 -0
  63. package/src/index.ts +16 -0
  64. package/src/sheets/CasualSheets.tsx +319 -0
  65. package/src/sheets/CasualSheetsIframe.tsx +220 -0
  66. package/src/sheets/api.ts +108 -0
  67. package/src/sheets/index.ts +11 -0
  68. package/src/signing/SigningPane.tsx +374 -0
  69. package/src/signing/SigningProvider.tsx +126 -0
  70. package/src/signing/captures.tsx +316 -0
  71. package/src/signing/controller.ts +151 -0
  72. package/src/signing/controller.unit.test.ts +133 -0
  73. package/src/signing/index.ts +44 -0
  74. package/src/signing/types.ts +89 -0
  75. package/src/styles.ts +16 -0
  76. package/src/univer/index.ts +17 -0
  77. package/src/univer/lazy-plugins.ts +280 -0
  78. package/src/xlsx/_perf.ts +14 -0
  79. package/src/xlsx/_snapshot-constants.ts +15 -0
  80. package/src/xlsx/comments-resource.ts +209 -0
  81. package/src/xlsx/constants.ts +9 -0
  82. package/src/xlsx/data-validation-resource.ts +219 -0
  83. package/src/xlsx/import.ts +35 -0
  84. package/src/xlsx/index.ts +40 -0
  85. package/src/xlsx/page-setup-resource.ts +205 -0
  86. package/src/xlsx/parse-impl.ts +418 -0
  87. package/src/xlsx/parse-in-worker.ts +82 -0
  88. package/src/xlsx/parser.worker.ts +39 -0
  89. package/src/xlsx/passthrough-resource.ts +175 -0
  90. package/src/xlsx/pivot-passthrough.ts +359 -0
  91. package/src/xlsx/style-mapping.ts +171 -0
  92. package/src/xlsx/tables-resource.ts +211 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Signing types — mirror the iframe-protocol envelopes from
3
+ * `docs/internal/13-iframe-protocol.md` so the SDK and iframe
4
+ * deliveries can hand the same payloads back and forth.
5
+ *
6
+ * Uniform across `app: 'docs' | 'sheet'`. Only the `anchor`
7
+ * discriminator changes.
8
+ */
9
+
10
+ export type SignatureMethod = 'drawn' | 'typed' | 'uploaded';
11
+
12
+ export type SignatureMode = 'sequential' | 'concurrent';
13
+
14
+ /**
15
+ * Doc-anchored field — paragraph id from the editor's `w14:paraId`,
16
+ * with an optional sub-paragraph search for placing the signature
17
+ * inside a phrase rather than a whole block.
18
+ */
19
+ export interface DocAnchor {
20
+ kind: 'doc';
21
+ paraId: string;
22
+ search?: string;
23
+ }
24
+
25
+ /** Sheet-anchored field — `sheet` name + A1-style `cell` ref. */
26
+ export interface SheetAnchor {
27
+ kind: 'sheet';
28
+ sheet: string;
29
+ cell: string;
30
+ }
31
+
32
+ export type SignatureAnchor = DocAnchor | SheetAnchor;
33
+
34
+ export interface SignatureField {
35
+ /** Per-field id supplied by the host; echoed on every progress event. */
36
+ fieldId: string;
37
+ /** Label rendered next to the field — "Employee signature", etc. */
38
+ label: string;
39
+ /** Required fields must complete before `onComplete` fires. */
40
+ required: boolean;
41
+ /** Where the signature lands in the document. */
42
+ anchor: SignatureAnchor;
43
+ /** Allowed signature methods. */
44
+ methods: SignatureMethod[];
45
+ /** Optional signer identity the host knows about. */
46
+ signer?: { name?: string; email?: string };
47
+ }
48
+
49
+ /** Payload emitted when a signer completes one field. */
50
+ export interface SignedFieldPayload {
51
+ fieldId: string;
52
+ method: SignatureMethod;
53
+ /** Raw signature material — PNG for drawn, UTF-8 string-as-bytes for typed, host-attested bytes for uploaded. */
54
+ bytes: ArrayBuffer;
55
+ mime: string;
56
+ /** Client wall-clock at completion; host typically pairs with a server timestamp. */
57
+ signedAt: string;
58
+ /** Optional placement hint a host generating a flat PDF can use. */
59
+ placement?: { page: number; xPct: number; yPct: number };
60
+ }
61
+
62
+ /** Payload emitted when all required fields are signed. */
63
+ export interface SignatureCompletePayload {
64
+ fieldIds: string[];
65
+ /** Document bytes WITH stamps applied — note (v1): the editor
66
+ * returns the unmodified document and a `fields` map so the
67
+ * HOST stamps the final bytes. v2 lands editor-side stamping. */
68
+ bytes: ArrayBuffer;
69
+ fields: Record<string, SignedFieldPayload>;
70
+ }
71
+
72
+ export type CancelReason = 'signer_cancelled' | 'session_expired' | 'host_aborted';
73
+
74
+ /**
75
+ * Configuration the host hands the editor when opening a signing
76
+ * session. Maps 1:1 to the iframe `signature.request` envelope.
77
+ */
78
+ export interface SigningSessionConfig {
79
+ fields: SignatureField[];
80
+ mode: SignatureMode;
81
+ /** Optional banner the editor renders at the top of the pane. */
82
+ banner?: string;
83
+ /** Fires once per field as the signer completes it. */
84
+ onFieldSigned?: (payload: SignedFieldPayload) => void | Promise<void>;
85
+ /** Fires after every required field is signed. */
86
+ onComplete?: (payload: SignatureCompletePayload) => void | Promise<void>;
87
+ /** Fires when either side aborts the session. */
88
+ onCancel?: (payload: { reason: CancelReason }) => void;
89
+ }
package/src/styles.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Univer plugin CSS — side-effect-only imports for the eager plugin
3
+ * set CasualSheets boots. Hosts import this once at app boot:
4
+ *
5
+ * import '@casualoffice/sheets/styles';
6
+ *
7
+ * If the host adds lazy plugins (sort, filter, drawing, comments,
8
+ * conditional formatting, …) it imports the corresponding CSS
9
+ * separately — those plugins ship their own /lib/index.css.
10
+ */
11
+ import '@univerjs/design/lib/index.css';
12
+ import '@univerjs/ui/lib/index.css';
13
+ import '@univerjs/docs-ui/lib/index.css';
14
+ import '@univerjs/sheets-ui/lib/index.css';
15
+ import '@univerjs/sheets-formula-ui/lib/index.css';
16
+ import '@univerjs/sheets-numfmt-ui/lib/index.css';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Internal Univer wiring shared by the SDK editor and the host app.
3
+ *
4
+ * Phase 1 (Batch 1) of the SDK migration lifts the lazy plugin loader out of
5
+ * `apps/web` so the editor can eventually run entirely from `@casualoffice/sheets`.
6
+ * It's pure Univer/DI code (no React context, FileSource, collab, or routing) with
7
+ * module-level singleton state, so the host and the SDK must resolve this one
8
+ * module instance. Later batches add the facade-coupled helpers (dev-helpers,
9
+ * paste-merge-hook, zoom override) + the editor core. See
10
+ * `docs/SDK_MIGRATION_PIPELINE.md` Phase 1.
11
+ *
12
+ * @internal — not part of the SDK's semver surface; consumers use `<CasualSheets>`.
13
+ */
14
+
15
+ // Lazy plugin loader (CF, DV, hyperlink, note, table, thread-comment, drawing,
16
+ // sort, filter, find-replace) + the module-level Univer holder.
17
+ export * from './lazy-plugins';
@@ -0,0 +1,280 @@
1
+ import type { Univer, IWorkbookData } from '@univerjs/core';
2
+
3
+ /**
4
+ * Lazy plugin loading — pipeline Stage 4. The cheap plugins (render,
5
+ * formula, sheets, sheets-ui, numfmt, docs) stay eager because every
6
+ * workbook needs them. The heavy / feature-specific plugins (CF, DV,
7
+ * hyperlink, table, note, thread-comment, drawing, sort, filter,
8
+ * find-replace) load on demand:
9
+ *
10
+ * 1. Eagerly when a snapshot is about to mount and we detect the
11
+ * plugin's resource key on it (e.g. `SHEET_CONDITIONAL_FORMATTING_PLUGIN`
12
+ * in `data.resources`). This is the safety net: missing this would
13
+ * silently drop plugin data on file open.
14
+ * 2. Lazily when the user reaches for the feature — opens the Data
15
+ * tab (sort/filter), hits Ctrl+F (find-replace), uses the Insert
16
+ * tab (drawing), etc. The shell hooks `ensurePlugin(...)` into
17
+ * these triggers and awaits before the action runs.
18
+ *
19
+ * Each loader returns the plugins-to-register in the correct order
20
+ * (base before UI, same as `plugins.ts`). Registration order matters
21
+ * in Univer; the loaders bundle a small group together to keep that
22
+ * locality explicit.
23
+ */
24
+
25
+ export type LazyPluginGroup =
26
+ | 'cf'
27
+ | 'dv'
28
+ | 'hyperlink'
29
+ | 'note'
30
+ | 'table'
31
+ | 'threadComment'
32
+ | 'drawing'
33
+ | 'sort'
34
+ | 'filter'
35
+ | 'findReplace';
36
+
37
+ type Loader = () => Promise<Array<[unknown, unknown?]>>;
38
+
39
+ const LOADERS: Record<LazyPluginGroup, Loader> = {
40
+ cf: async () => {
41
+ const [base, ui] = await Promise.all([
42
+ import('@univerjs/sheets-conditional-formatting'),
43
+ import('@univerjs/sheets-conditional-formatting-ui'),
44
+ ]);
45
+ return [
46
+ [base.UniverSheetsConditionalFormattingPlugin],
47
+ [ui.UniverSheetsConditionalFormattingUIPlugin],
48
+ ];
49
+ },
50
+ dv: async () => {
51
+ const [base, ui] = await Promise.all([
52
+ import('@univerjs/sheets-data-validation'),
53
+ import('@univerjs/sheets-data-validation-ui'),
54
+ ]);
55
+ return [
56
+ [base.UniverSheetsDataValidationPlugin],
57
+ [ui.UniverSheetsDataValidationUIPlugin],
58
+ ];
59
+ },
60
+ hyperlink: async () => {
61
+ const [base, ui] = await Promise.all([
62
+ import('@univerjs/sheets-hyper-link'),
63
+ import('@univerjs/sheets-hyper-link-ui'),
64
+ ]);
65
+ return [
66
+ [base.UniverSheetsHyperLinkPlugin],
67
+ [ui.UniverSheetsHyperLinkUIPlugin],
68
+ ];
69
+ },
70
+ note: async () => {
71
+ const [base, ui] = await Promise.all([
72
+ import('@univerjs/sheets-note'),
73
+ import('@univerjs/sheets-note-ui'),
74
+ ]);
75
+ return [[base.UniverSheetsNotePlugin], [ui.UniverSheetsNoteUIPlugin]];
76
+ },
77
+ table: async () => {
78
+ const [base, ui] = await Promise.all([
79
+ import('@univerjs/sheets-table'),
80
+ import('@univerjs/sheets-table-ui'),
81
+ ]);
82
+ return [[base.UniverSheetsTablePlugin], [ui.UniverSheetsTableUIPlugin]];
83
+ },
84
+ threadComment: async () => {
85
+ const [tc, tcUi, sheetsTc, sheetsTcUi] = await Promise.all([
86
+ import('@univerjs/thread-comment'),
87
+ import('@univerjs/thread-comment-ui'),
88
+ import('@univerjs/sheets-thread-comment'),
89
+ import('@univerjs/sheets-thread-comment-ui'),
90
+ ]);
91
+ return [
92
+ [tc.UniverThreadCommentPlugin],
93
+ [tcUi.UniverThreadCommentUIPlugin],
94
+ [sheetsTc.UniverSheetsThreadCommentPlugin],
95
+ [sheetsTcUi.UniverSheetsThreadCommentUIPlugin],
96
+ ];
97
+ },
98
+ drawing: async () => {
99
+ const [d, dUi, sd, sdUi] = await Promise.all([
100
+ import('@univerjs/drawing'),
101
+ import('@univerjs/drawing-ui'),
102
+ import('@univerjs/sheets-drawing'),
103
+ import('@univerjs/sheets-drawing-ui'),
104
+ // Side-effect imports: install FWorksheet.insertImage / getImages /
105
+ // updateImages on the facade prototype. Without these, code that
106
+ // reaches in via the FUniver facade (e2e specs, future shell glue)
107
+ // sees an undefined method even though the plugin is registered.
108
+ import('@univerjs/sheets-drawing/facade'),
109
+ import('@univerjs/sheets-drawing-ui/facade'),
110
+ ]);
111
+ return [
112
+ [d.UniverDrawingPlugin],
113
+ [dUi.UniverDrawingUIPlugin],
114
+ [sd.UniverSheetsDrawingPlugin],
115
+ [sdUi.UniverSheetsDrawingUIPlugin],
116
+ ];
117
+ },
118
+ sort: async () => {
119
+ const [base, ui] = await Promise.all([
120
+ import('@univerjs/sheets-sort'),
121
+ import('@univerjs/sheets-sort-ui'),
122
+ ]);
123
+ return [[base.UniverSheetsSortPlugin], [ui.UniverSheetsSortUIPlugin]];
124
+ },
125
+ filter: async () => {
126
+ const [base, ui] = await Promise.all([
127
+ import('@univerjs/sheets-filter'),
128
+ import('@univerjs/sheets-filter-ui'),
129
+ ]);
130
+ return [[base.UniverSheetsFilterPlugin], [ui.UniverSheetsFilterUIPlugin]];
131
+ },
132
+ findReplace: async () => {
133
+ const [base, sheets] = await Promise.all([
134
+ import('@univerjs/find-replace'),
135
+ import('@univerjs/sheets-find-replace'),
136
+ ]);
137
+ return [[base.UniverFindReplacePlugin], [sheets.UniverSheetsFindReplacePlugin]];
138
+ },
139
+ };
140
+
141
+ /**
142
+ * Map from a Univer `data.resources[].name` to the lazy group that owns
143
+ * it. Used by `eagerLoadForSnapshot` to pre-register plugins whose
144
+ * state already lives on the workbook so file-open never silently
145
+ * drops a CF rule / table / drawing / etc.
146
+ */
147
+ const RESOURCE_NAME_TO_GROUP: Record<string, LazyPluginGroup> = {
148
+ SHEET_CONDITIONAL_FORMATTING_PLUGIN: 'cf',
149
+ SHEET_DATA_VALIDATION_PLUGIN: 'dv',
150
+ SHEET_HYPER_LINK_PLUGIN: 'hyperlink',
151
+ SHEET_NOTE_PLUGIN: 'note',
152
+ SHEET_TABLE_PLUGIN: 'table',
153
+ SHEET_THREAD_COMMENT_BASE_PLUGIN: 'threadComment',
154
+ SHEET_DRAWING_PLUGIN: 'drawing',
155
+ SHEET_SORT_PLUGIN: 'sort',
156
+ SHEET_FILTER_PLUGIN: 'filter',
157
+ };
158
+
159
+ const loaded = new Set<LazyPluginGroup>();
160
+ const inflight = new Map<LazyPluginGroup, Promise<void>>();
161
+
162
+ /**
163
+ * Module-level reference to the live Univer instance, set by
164
+ * `UniverSheet.tsx` immediately after `new Univer()`. Lets shell code
165
+ * call `ensurePluginByName(group)` without plumbing the Univer
166
+ * instance through React context (the FUniver facade doesn't expose
167
+ * its host). Cleared on dispose so callers fail loudly if they hit
168
+ * the lazy path after teardown.
169
+ */
170
+ let currentUniver: Univer | null = null;
171
+
172
+ export function setUniverForLazyLoad(univer: Univer | null): void {
173
+ currentUniver = univer;
174
+ }
175
+
176
+ /**
177
+ * Idempotent: subsequent calls for the same group resolve immediately
178
+ * if the plugin is already loaded; concurrent calls share the same
179
+ * in-flight promise so we never double-register.
180
+ */
181
+ export function ensurePlugin(univer: Univer, group: LazyPluginGroup): Promise<void> {
182
+ if (loaded.has(group)) return Promise.resolve();
183
+ const existing = inflight.get(group);
184
+ if (existing) return existing;
185
+ const loader = LOADERS[group];
186
+ if (!loader) return Promise.resolve();
187
+ const p = loader().then((plugins) => {
188
+ for (const [PluginCtor, config] of plugins) {
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
190
+ univer.registerPlugin(PluginCtor as any, config);
191
+ }
192
+ loaded.add(group);
193
+ inflight.delete(group);
194
+ });
195
+ inflight.set(group, p);
196
+ return p;
197
+ }
198
+
199
+ /**
200
+ * Shell-friendly variant of `ensurePlugin` that pulls the Univer
201
+ * instance from the module-level holder set by `UniverSheet.tsx`. Use
202
+ * this anywhere we only have the FUniver facade (toolbar callbacks,
203
+ * panel handlers). Returns a resolved promise if the holder is empty —
204
+ * the worst case is a no-op, never a throw.
205
+ */
206
+ export function ensurePluginByName(group: LazyPluginGroup): Promise<void> {
207
+ if (loaded.has(group)) return Promise.resolve();
208
+ if (!currentUniver) return Promise.resolve();
209
+ return ensurePlugin(currentUniver, group);
210
+ }
211
+
212
+ /**
213
+ * Walk a snapshot for plugin-owned resources + side-channel hyperlinks
214
+ * and eagerly load every group that's referenced. Returns a promise
215
+ * the caller MUST await before `createUnit` — Univer's resource manager
216
+ * silently discards keys for plugins that aren't yet registered.
217
+ */
218
+ export async function eagerLoadForSnapshot(univer: Univer, snapshot: IWorkbookData): Promise<void> {
219
+ const groups = new Set<LazyPluginGroup>();
220
+ const resources = snapshot.resources ?? [];
221
+ for (const r of resources) {
222
+ const g = RESOURCE_NAME_TO_GROUP[r.name];
223
+ if (g) groups.add(g);
224
+ }
225
+ // Hyperlinks live inline in cell.p (Stage 5) — the hyperlink plugin
226
+ // still owns click handling / context-menu actions, so eager-load
227
+ // when any cell has a HYPERLINK customRange.
228
+ if (snapshotHasHyperlinks(snapshot)) groups.add('hyperlink');
229
+ await Promise.all(Array.from(groups).map((g) => ensurePlugin(univer, g)));
230
+ }
231
+
232
+ /**
233
+ * Idle-load every remaining lazy group AFTER Univer is mounted. The
234
+ * bundle split (each group ships as its own chunk) is the persistent
235
+ * boot-time win — the initial paint doesn't pay for them. This
236
+ * idle-load just ensures they're all eventually registered so a user
237
+ * clicking "Insert > Table" doesn't hit a no-op.
238
+ *
239
+ * Loads in parallel via `requestIdleCallback` so they don't compete
240
+ * with the first paint, falling back to `setTimeout(0)` in browsers
241
+ * without rIC (Safari).
242
+ */
243
+ export function idleLoadAll(univer: Univer): void {
244
+ const groups = Object.keys(LOADERS) as LazyPluginGroup[];
245
+ schedule(() => {
246
+ for (const g of groups) {
247
+ void ensurePlugin(univer, g);
248
+ }
249
+ });
250
+ }
251
+
252
+ function schedule(fn: () => void): void {
253
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
+ const ric = (globalThis as any).requestIdleCallback as
255
+ | ((cb: () => void, opts?: { timeout: number }) => number)
256
+ | undefined;
257
+ if (ric) ric(fn, { timeout: 500 });
258
+ else setTimeout(fn, 0);
259
+ }
260
+
261
+ function snapshotHasHyperlinks(snapshot: IWorkbookData): boolean {
262
+ const sheetOrder = snapshot.sheetOrder ?? [];
263
+ for (const sid of sheetOrder) {
264
+ const sheet = snapshot.sheets?.[sid];
265
+ if (!sheet?.cellData) continue;
266
+ const cellData = sheet.cellData as Record<
267
+ string,
268
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
269
+ Record<string, { p?: any }>
270
+ >;
271
+ for (const r of Object.keys(cellData)) {
272
+ const row = cellData[r];
273
+ for (const c of Object.keys(row)) {
274
+ const ranges = row[c]?.p?.body?.customRanges ?? [];
275
+ if (ranges.some((cr: { rangeType?: number }) => cr.rangeType === 0)) return true;
276
+ }
277
+ }
278
+ }
279
+ return false;
280
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tiny perf shim — drop-in replacement for apps/web's `../perf` so the
3
+ * SDK doesn't have to pull in a User Timing dependency. The host can
4
+ * wrap individual SDK calls with its own timing if it wants spans;
5
+ * inside the SDK they're cheap no-op forwards.
6
+ */
7
+
8
+ export function timeIt<T>(_label: string, fn: () => T): T {
9
+ return fn();
10
+ }
11
+
12
+ export async function timeItAsync<T>(_label: string, fn: () => Promise<T>): Promise<T> {
13
+ return fn();
14
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Snapshot defaults pulled out of apps/web/src/snapshot.ts so the
3
+ * SDK's xlsx parser doesn't have to import that whole module. Keep
4
+ * these in sync with the host snapshot module if the defaults drift.
5
+ *
6
+ * UNIVER_VERSION must match the runtime Univer the host boots — the
7
+ * appVersion field on the IWorkbookData snapshot is checked at unit
8
+ * mount and a mismatch warns in dev. Sheet apps' `../snapshot` reads
9
+ * the version from the workspace's @univerjs/core dep; we hardcode
10
+ * the same minor here because the SDK declares @univerjs/* as
11
+ * `^0.24.0` peer.
12
+ */
13
+ export const INITIAL_ROWS = 1024;
14
+ export const INITIAL_COLUMNS = 26;
15
+ export const UNIVER_VERSION = '0.24.0';
@@ -0,0 +1,209 @@
1
+ import type ExcelJS from 'exceljs';
2
+ import type { IWorkbookData } from '@univerjs/core';
3
+
4
+ /**
5
+ * xlsx-native `cell.note` ⇄ Univer `thread-comment` resource bridge.
6
+ *
7
+ * Comments in xlsx live on the cell itself (`xl/comments<N>.xml`).
8
+ * Univer's thread-comment plugin stores them in a workbook-level
9
+ * resource keyed by sheet id under the name below — same
10
+ * registration pattern as defined-names. Each side of the round-trip
11
+ * has to translate to the shape the other one expects:
12
+ *
13
+ * xlsx cell.note (string | { texts: [{ text }] })
14
+ * ⇅
15
+ * { dataStream: "<text>\r\n" } (Univer IDocumentBody minimal form)
16
+ *
17
+ * Why this exists separately from the parser/exporter modules:
18
+ * 1. Two-sided code lives next to itself so the inverse stays
19
+ * obvious. The audit test fails the moment one side drifts.
20
+ * 2. The `IThreadComment` row builder is non-trivial and reused
21
+ * verbatim on both sides — extracting it keeps parse-impl and
22
+ * export-impl from carrying near-duplicate shape definitions.
23
+ */
24
+
25
+ // Mirrors vendor/univer/packages/thread-comment/src/controllers/tc-resource.controller.ts:25
26
+ // — the resource registration name the plugin's controller subscribes
27
+ // to at workbook load. Hard-coding instead of importing avoids pulling
28
+ // the plugin into the xlsx worker bundle (thread-comment ships UI deps
29
+ // that don't tree-shake cleanly in a Worker context).
30
+ export const THREAD_COMMENT_RESOURCE = 'SHEET_UNIVER_THREAD_COMMENT_PLUGIN';
31
+
32
+ // Minimal subset of Univer's `IThreadComment` we synthesise. The
33
+ // plugin populates the rest (children, mentions, resolved) on
34
+ // subsequent edits; xlsx-native comments don't carry any of them.
35
+ export type SynthComment = {
36
+ id: string;
37
+ threadId: string;
38
+ ref: string; // "B2" style
39
+ dT: string; // ISO date
40
+ personId: string;
41
+ text: { dataStream: string };
42
+ unitId: string;
43
+ subUnitId: string;
44
+ // children: [] on import — Univer's controller tolerates this
45
+ // because `addComment` of a root with no children is the normal
46
+ // create path.
47
+ children?: never[];
48
+ };
49
+
50
+ const PERSON_FROM_XLSX = 'imported';
51
+
52
+ function colToLetters(n: number): string {
53
+ let out = '';
54
+ let v = n;
55
+ while (v >= 0) {
56
+ out = String.fromCharCode(65 + (v % 26)) + out;
57
+ v = Math.floor(v / 26) - 1;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function refOf(row: number, column: number): string {
63
+ return `${colToLetters(column)}${row + 1}`;
64
+ }
65
+
66
+ function noteToString(note: unknown): string | null {
67
+ if (typeof note === 'string') return note.trim() ? note : null;
68
+ if (note && typeof note === 'object') {
69
+ // ExcelJS's expanded shape: { texts: [{ text }] }. Fall back to
70
+ // `.text` if a single-string variant came back through some
71
+ // odder code path.
72
+ const texts = (note as { texts?: Array<{ text?: string }> }).texts;
73
+ if (Array.isArray(texts)) {
74
+ const joined = texts.map((t) => t?.text ?? '').join('');
75
+ return joined.trim() ? joined : null;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Walk every worksheet and collect xlsx-native `cell.note` entries
83
+ * into a Univer thread-comment resource payload. The synthesised IDs
84
+ * and dates aren't stable — re-opening the same file produces new
85
+ * ones — which is fine for the use cases we care about (comments
86
+ * survive the round-trip; nobody is referring to them by id).
87
+ */
88
+ export function readCommentsFromXlsx(
89
+ wb: ExcelJS.Workbook,
90
+ unitId: string,
91
+ sheetIdForExcel: (excelId: number) => string,
92
+ ): Record<string, SynthComment[]> {
93
+ const out: Record<string, SynthComment[]> = {};
94
+ let seq = 0;
95
+ const nowIso = new Date().toISOString();
96
+
97
+ for (const ws of wb.worksheets) {
98
+ const subUnitId = sheetIdForExcel(ws.id);
99
+ const bucket: SynthComment[] = [];
100
+ ws.eachRow({ includeEmpty: false }, (row, rowNumber) => {
101
+ row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
102
+ // ExcelJS exposes notes on cells with comments — the field is
103
+ // missing entirely on the vast majority of cells, so iterating
104
+ // every cell is cheap.
105
+ const note = (cell as unknown as { note?: unknown }).note;
106
+ const text = noteToString(note);
107
+ if (!text) return;
108
+ const id = `xc-${unitId}-${seq++}`;
109
+ bucket.push({
110
+ id,
111
+ threadId: id, // single-comment thread; thread id = root id
112
+ ref: refOf(rowNumber - 1, colNumber - 1),
113
+ dT: nowIso,
114
+ personId: PERSON_FROM_XLSX,
115
+ // Univer's body convention terminates with `\r\n` (one paragraph).
116
+ // Without it the cursor in the panel jumps a column on first
117
+ // open of the comment.
118
+ text: { dataStream: `${text}\r\n` },
119
+ unitId,
120
+ subUnitId,
121
+ children: [],
122
+ });
123
+ });
124
+ });
125
+ if (bucket.length > 0) out[subUnitId] = bucket;
126
+ }
127
+
128
+ return out;
129
+ }
130
+
131
+ /**
132
+ * Merge a synthesised comment payload into the workbook's `resources`
133
+ * array. Skipped when the workbook already carries a thread-comment
134
+ * resource from our hidden sidecar (`__casual_sheets_resources__`) —
135
+ * that one has the full plugin shape; we shouldn't clobber it with a
136
+ * lossy xlsx-native re-derivation.
137
+ */
138
+ export function mergeCommentsIntoResources(
139
+ resources: IWorkbookData['resources'],
140
+ comments: Record<string, SynthComment[]>,
141
+ ): IWorkbookData['resources'] {
142
+ if (Object.keys(comments).length === 0) return resources;
143
+ const existing = resources?.find((r) => r.name === THREAD_COMMENT_RESOURCE);
144
+ if (existing) return resources;
145
+ const next = [...(resources ?? [])];
146
+ next.push({
147
+ name: THREAD_COMMENT_RESOURCE,
148
+ data: JSON.stringify(comments),
149
+ });
150
+ return next;
151
+ }
152
+
153
+ /**
154
+ * Read the thread-comment resource off a snapshot. Tolerant of older
155
+ * / missing / malformed payloads — those cases return `{}` so the
156
+ * exporter just skips writing notes rather than throwing on save.
157
+ */
158
+ export function readCommentsFromSnapshot(
159
+ data: IWorkbookData,
160
+ ): Record<string, SynthComment[]> {
161
+ const entry = data.resources?.find((r) => r.name === THREAD_COMMENT_RESOURCE);
162
+ if (!entry?.data) return {};
163
+ try {
164
+ const parsed = JSON.parse(entry.data) as Record<string, unknown>;
165
+ if (!parsed || typeof parsed !== 'object') return {};
166
+ const out: Record<string, SynthComment[]> = {};
167
+ for (const [sheetId, value] of Object.entries(parsed)) {
168
+ if (!Array.isArray(value)) continue;
169
+ const bucket: SynthComment[] = [];
170
+ for (const c of value) {
171
+ if (!c || typeof c !== 'object') continue;
172
+ const obj = c as Record<string, unknown>;
173
+ const text = (obj.text as { dataStream?: string } | undefined)?.dataStream;
174
+ const ref = typeof obj.ref === 'string' ? obj.ref : null;
175
+ if (!ref || typeof text !== 'string') continue;
176
+ bucket.push(c as SynthComment);
177
+ }
178
+ if (bucket.length > 0) out[sheetId] = bucket;
179
+ }
180
+ return out;
181
+ } catch {
182
+ return {};
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Extract the plain string from a Univer comment body. Strips the
188
+ * trailing `\r\n` (or `\n`) that the body convention appends so the
189
+ * xlsx-side note doesn't show as a blank line below the text in
190
+ * Excel's pop-up.
191
+ */
192
+ export function commentBodyToString(body: SynthComment['text']): string {
193
+ const s = body?.dataStream ?? '';
194
+ return s.replace(/[\r\n]+$/, '');
195
+ }
196
+
197
+ /**
198
+ * Parse an "A1"-style ref back to a zero-based (row, col). Falls back
199
+ * to (-1, -1) on malformed input — exporter callers should skip in
200
+ * that case.
201
+ */
202
+ export function refToRowCol(ref: string): { row: number; column: number } {
203
+ const m = /^([A-Z]+)(\d+)$/.exec(ref);
204
+ if (!m) return { row: -1, column: -1 };
205
+ const letters = m[1];
206
+ let col = 0;
207
+ for (let i = 0; i < letters.length; i++) col = col * 26 + (letters.charCodeAt(i) - 64);
208
+ return { row: Number(m[2]) - 1, column: col - 1 };
209
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Name of a hidden worksheet we use to stash JSON we can't represent
3
+ * natively in xlsx (Univer plugin state — e.g. table definitions, outline
4
+ * groups). On open we recognize and consume it, never showing it to the
5
+ * user. Defined in its own module so both the parser worker and the
6
+ * exporter worker can import it without dragging the other one's
7
+ * ExcelJS code into their bundle.
8
+ */
9
+ export const RESOURCES_SHEET = '__casual_sheets_resources__';