@rowsncolumns/rnc-engine 0.10.132

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 (37) hide show
  1. package/LICENSE.md +100 -0
  2. package/README.md +83 -0
  3. package/dist/esm/command-handlers.d.ts +169 -0
  4. package/dist/esm/command-handlers.d.ts.map +1 -0
  5. package/dist/esm/command-handlers.js +404 -0
  6. package/dist/esm/fold.d.ts +140 -0
  7. package/dist/esm/fold.d.ts.map +1 -0
  8. package/dist/esm/fold.js +539 -0
  9. package/dist/esm/index.d.ts +620 -0
  10. package/dist/esm/index.d.ts.map +1 -0
  11. package/dist/esm/index.js +425 -0
  12. package/dist/esm/io.d.ts +119 -0
  13. package/dist/esm/io.d.ts.map +1 -0
  14. package/dist/esm/io.js +318 -0
  15. package/dist/esm/keys.d.ts +20 -0
  16. package/dist/esm/keys.d.ts.map +1 -0
  17. package/dist/esm/keys.js +41 -0
  18. package/dist/esm/move-rescope.d.ts +18 -0
  19. package/dist/esm/move-rescope.d.ts.map +1 -0
  20. package/dist/esm/move-rescope.js +38 -0
  21. package/dist/esm/use-engine-core.d.ts +293 -0
  22. package/dist/esm/use-engine-core.d.ts.map +1 -0
  23. package/dist/esm/use-engine-core.js +2279 -0
  24. package/dist/esm/use-engine-pivot-editor.d.ts +78 -0
  25. package/dist/esm/use-engine-pivot-editor.d.ts.map +1 -0
  26. package/dist/esm/use-engine-pivot-editor.js +252 -0
  27. package/dist/esm/use-spreadsheet-engine.d.ts +7 -0
  28. package/dist/esm/use-spreadsheet-engine.d.ts.map +1 -0
  29. package/dist/esm/use-spreadsheet-engine.js +23 -0
  30. package/dist/esm/use-spreadsheet-ui.d.ts +39 -0
  31. package/dist/esm/use-spreadsheet-ui.d.ts.map +1 -0
  32. package/dist/esm/use-spreadsheet-ui.js +78 -0
  33. package/dist/esm/wasm/rnc_wasm.d.ts +394 -0
  34. package/dist/esm/wasm/rnc_wasm.js +1455 -0
  35. package/dist/esm/wasm/rnc_wasm_bg.wasm +0 -0
  36. package/dist/esm/wasm/rnc_wasm_bg.wasm.d.ts +81 -0
  37. package/package.json +51 -0
@@ -0,0 +1,425 @@
1
+ import { cellKey } from "./keys";
2
+ import init, { CollabSpreadsheetEngine, SpreadsheetEngine, } from "./wasm/rnc_wasm.js";
3
+ // The wasm module initializes once per page.
4
+ let initPromise = null;
5
+ /** Initialize the wasm module (idempotent). Called by {@link RncEngine.create}; exported for control. */
6
+ export function initEngine() {
7
+ return (initPromise ??= init());
8
+ }
9
+ /**
10
+ * Ergonomic, typed handle to a standalone in-memory spreadsheet engine.
11
+ * Commands go in; value/style change-sets and windowed reads come out — all using the exact
12
+ * `rnc-model` shapes the rest of the app already speaks.
13
+ */
14
+ export class RncEngine {
15
+ inner;
16
+ listeners = new Set();
17
+ constructor(inner) {
18
+ this.inner = inner;
19
+ }
20
+ /**
21
+ * Initialize wasm (idempotent) and create an engine with the given sheets registered.
22
+ *
23
+ * By default the backend is the single-user {@link SpreadsheetEngine}. Pass `options.engine` to
24
+ * inject a different store — e.g. `new CollabSpreadsheetEngine()` (yhub/collab) or your own wasm
25
+ * store implementing {@link SpreadsheetEngineLike}. When the store implements `initGrid`
26
+ * (positional/CRDT), each registered sheet is pre-sized to `gridRows`×`gridCols`.
27
+ */
28
+ static async create(sheets = [], options = {}) {
29
+ await initEngine();
30
+ const inner = options.engine ?? new SpreadsheetEngine();
31
+ const rows = options.gridRows ?? 1000;
32
+ const cols = options.gridCols ?? 26;
33
+ for (const sheet of sheets) {
34
+ inner.addSheet(sheet.sheetId, sheet.title);
35
+ // Positional/CRDT stores (collab) must pre-size the grid before cells are written; stores that
36
+ // grow on demand omit `initGrid`, so this is a no-op for them.
37
+ inner.initGrid?.(sheet.sheetId, rows, cols);
38
+ }
39
+ return new RncEngine(inner);
40
+ }
41
+ /**
42
+ * Wrap an ALREADY-INITIALIZED backing engine — skips {@link initEngine} (the caller owns wasm
43
+ * init). Use this when the wasm module is loaded by a different path than the vendored
44
+ * `--target web` build: a `--target nodejs` engine in tests/SSR, a `CollabSpreadsheetEngine` you
45
+ * constructed yourself, or any custom {@link SpreadsheetEngineLike} store. Registers `sheets` (and
46
+ * pre-sizes the grid via `initGrid` when the store supports it) exactly like {@link create}.
47
+ */
48
+ static fromEngine(inner, sheets = [], options = {}) {
49
+ const rows = options.gridRows ?? 1000;
50
+ const cols = options.gridCols ?? 26;
51
+ for (const sheet of sheets) {
52
+ inner.addSheet(sheet.sheetId, sheet.title);
53
+ inner.initGrid?.(sheet.sheetId, rows, cols);
54
+ }
55
+ return new RncEngine(inner);
56
+ }
57
+ /** Whether the backing engine is collaborative (implements the Yjs {@link CollabSync} surface). */
58
+ get isCollab() {
59
+ return (typeof this.inner.applyUpdate === "function");
60
+ }
61
+ collab() {
62
+ if (!this.isCollab)
63
+ throw new Error("RncEngine: backing engine is not collaborative (no Yjs sync surface)");
64
+ return this.inner;
65
+ }
66
+ /** Pre-size a sheet's grid on a positional/CRDT store. No-op for stores without `initGrid`. */
67
+ initGrid(sheetId, rows, cols) {
68
+ this.inner.initGrid?.(sheetId, rows, cols);
69
+ }
70
+ /** Register a sheet (`id` → display name). */
71
+ addSheet(sheetId, name) {
72
+ this.inner.addSheet(sheetId, name);
73
+ }
74
+ // ── Streaming hydration (for huge workbooks) ──────────────────────────────────────────────
75
+ // NDJSON streaming hydration (huge workbooks). Optional on the backend: the single-user engine
76
+ // supports it; a collab store hydrates from the CRDT update stream instead, so these no-op there.
77
+ /** Begin a streaming hydration. Feed NDJSON byte chunks with {@link streamWrite}, then {@link streamEnd}. */
78
+ streamBegin() {
79
+ this.inner.streamBegin?.();
80
+ }
81
+ /** Ingest one NDJSON byte chunk (boundaries may fall anywhere — partial lines are buffered). */
82
+ streamWrite(chunk) {
83
+ this.inner.streamWrite?.(chunk);
84
+ }
85
+ /** Finish hydration: ingest the trailing line and recompute once. Read the viewport via `readWindow`. */
86
+ streamEnd() {
87
+ this.inner.streamEnd?.();
88
+ }
89
+ /**
90
+ * Hydrate from a byte stream of NDJSON records — e.g. `(await fetch(url)).body`. The 100 MB+
91
+ * workbook flows network → wasm in chunks and is never held whole in JS. Records are
92
+ * `{"t":"sheet"|"cell"|"style", …}` (see the Rust `StreamRecord`). Nothing is returned; call
93
+ * `readWindow` for the visible area once this resolves.
94
+ */
95
+ async hydrateFromStream(stream) {
96
+ this.streamBegin();
97
+ const reader = stream.getReader();
98
+ try {
99
+ for (;;) {
100
+ const { done, value } = await reader.read();
101
+ if (done)
102
+ break;
103
+ if (value)
104
+ this.streamWrite(value);
105
+ }
106
+ }
107
+ finally {
108
+ reader.releaseLock();
109
+ }
110
+ this.streamEnd();
111
+ }
112
+ /**
113
+ * Hydrate the engine from an initial document (sheets + cells + styles, the shape your API
114
+ * returns). Recomputes formulas and delivers the full change-set to {@link onChanges} subscribers
115
+ * — so initial load uses the exact same path as every other change. Returns it inline too.
116
+ */
117
+ load(document) {
118
+ const commit = JSON.parse(this.inner.loadDocument(JSON.stringify(document)));
119
+ this.listeners.forEach((listener) => listener(commit));
120
+ return commit;
121
+ }
122
+ /**
123
+ * Apply a `commands.ts` command. The resulting change-set is delivered to every
124
+ * {@link onChanges} subscriber; it's also returned for callers that want it inline.
125
+ */
126
+ applyCommand(command) {
127
+ const commit = JSON.parse(this.inner.applyCommand(JSON.stringify(command)));
128
+ // The wasm `&mut self` borrow is released here (the call above already returned), so a
129
+ // subscriber may safely re-enter the engine (e.g. `readWindow`). That's why the subscriber
130
+ // lives in this TS layer, not as a wasm-bindgen callback (which would re-enter mid-borrow).
131
+ this.listeners.forEach((listener) => listener(commit));
132
+ return commit;
133
+ }
134
+ /**
135
+ * Declare whether the host renders WINDOWED, so the backing engine can omit the per-cell commit
136
+ * channels the windowed render discards (it re-reads the viewport via {@link readWindow}). Forwarded
137
+ * to the injected engine's `setWindowed` when it implements it; a no-op otherwise (the windowed fold
138
+ * still ignores those channels correctly — it just pays to transfer them). Called once by the
139
+ * windowed hook after engine creation. See {@link SpreadsheetEngineLike.setWindowed}.
140
+ */
141
+ setWindowed(on) {
142
+ this.inner.setWindowed?.(on);
143
+ }
144
+ /**
145
+ * Subscribe to the change-sets the engine produces. Returns an unsubscribe function.
146
+ *
147
+ * Today this fires synchronously after each {@link applyCommand}. The same API is the seam for
148
+ * out-of-band changes later — volatile recalc ticks, or Worker `postMessage` when calc moves off
149
+ * the main thread — without the UI's subscribe code changing.
150
+ */
151
+ onChanges(listener) {
152
+ this.listeners.add(listener);
153
+ return () => {
154
+ this.listeners.delete(listener);
155
+ };
156
+ }
157
+ /**
158
+ * Undo / redo the last document mutation in the engine. The resulting change-set (reverted input
159
+ * AND recomputed values, across any sheets touched) is delivered to {@link onChanges} subscribers.
160
+ */
161
+ undo() {
162
+ const commit = JSON.parse(this.inner.undo());
163
+ this.listeners.forEach((listener) => listener(commit));
164
+ return commit;
165
+ }
166
+ redo() {
167
+ const commit = JSON.parse(this.inner.redo());
168
+ this.listeners.forEach((listener) => listener(commit));
169
+ return commit;
170
+ }
171
+ /** Whether an undo / redo is currently available (for enabling toolbar buttons). */
172
+ canUndo() {
173
+ return this.inner.canUndo();
174
+ }
175
+ canRedo() {
176
+ return this.inner.canRedo();
177
+ }
178
+ /** Drop the entire undo/redo history (the document is unchanged). Backs `onClearHistory`. */
179
+ clearHistory() {
180
+ this.inner.clearHistory();
181
+ }
182
+ /** Undo / redo stack depth. A UI that keeps parallel per-edit state (cursor, selection, active
183
+ * sheet) watches these to push/pop its own snapshots in lockstep — a command that recorded an undo
184
+ * entry bumps `undoDepth`, so the UI knows whether to snapshot (and skips no-op-inverse commands). */
185
+ undoDepth() {
186
+ return this.inner.undoDepth();
187
+ }
188
+ redoDepth() {
189
+ return this.inner.redoDepth();
190
+ }
191
+ /** Read a 1-indexed, inclusive window of cells + consolidated styles. Prefers the LOSSLESS full-cell
192
+ * read (`readWindowCommit`): each cell is carried VERBATIM (`ue`/formula source, hyperlink, note, full
193
+ * `ev`) under `cell`, so the windowed render uses it as-is rather than reconstructing a lossy cell
194
+ * from a lean formatted/number/isFormula projection (which dropped `ue.fv` — the formula bar then
195
+ * showed the computed value, not `=SUM(4,4)`). Falls back to the lean `readWindow` for engines without
196
+ * it. The window is only viewport-sized, so carrying whole cells costs nothing. */
197
+ readWindow(sheetId, startRow, startCol, endRow, endCol) {
198
+ const inner = this.inner;
199
+ if (typeof inner.readWindowCommit === "function") {
200
+ const commit = JSON.parse(inner.readWindowCommit(sheetId, startRow, startCol, endRow, endCol));
201
+ return {
202
+ sheetId,
203
+ startRowIndex: startRow,
204
+ startColumnIndex: startCol,
205
+ endRowIndex: endRow,
206
+ endColumnIndex: endCol,
207
+ cells: (commit.changes ?? [])
208
+ .filter((ch) => ch.cell != null)
209
+ .map((ch) => ({
210
+ rowIndex: ch.rowIndex,
211
+ columnIndex: ch.columnIndex,
212
+ formatted: ch.cell?.fv ?? "",
213
+ number: ch.cell?.ev?.nv ?? null,
214
+ isFormula: ch.cell?.ue?.fv != null,
215
+ cell: ch.cell ?? undefined,
216
+ })),
217
+ styles: commit.styleChanges ?? [],
218
+ };
219
+ }
220
+ return JSON.parse(inner.readWindow(sheetId, startRow, startCol, endRow, endCol));
221
+ }
222
+ /**
223
+ * Selection statistics (sum / count / numericalCount / average / min / max) over a 1-indexed
224
+ * inclusive {@link SheetRange} — what the status bar shows for a selection. Computed in Rust over
225
+ * the stored cells, so it stays cheap even on a million-cell selection (no value round-trip to JS,
226
+ * no main-thread iteration — the reason the old JS path refused selections over ~200k cells).
227
+ *
228
+ * Returns `null` if the backing engine doesn't expose `rangeStats` (the host then falls back to
229
+ * its own JS computation). Numeric fields are `null` when the range holds no numbers.
230
+ */
231
+ rangeStats(range) {
232
+ const json = this.inner.rangeStats?.(range.sheetId, range.startRowIndex, range.startColumnIndex, range.endRowIndex, range.endColumnIndex);
233
+ if (json == null)
234
+ return null;
235
+ return JSON.parse(json);
236
+ }
237
+ /** Off-window search: every cell on `sheetId` whose display text matches `find`, returned row-major
238
+ * (the order next/prev navigation walks). The windowed UI's JS search only sees the loaded viewport,
239
+ * so matches OUTSIDE it must come from the engine, which holds the whole document. `ranges` restricts
240
+ * the scope (whole sheet when omitted). Match-case / whole-word (`\b`) / regex mirror the JS search.
241
+ * Returns `[]` if the find is empty or the backing engine can't search. */
242
+ findAll(sheetId, find, options, ranges) {
243
+ if (!find || !this.inner.findAll)
244
+ return [];
245
+ const json = this.inner.findAll(sheetId, ranges && ranges.length ? JSON.stringify(ranges) : "", find, options?.matchCase ?? false, options?.wholeWord ?? false, options?.useRegex ?? false);
246
+ return JSON.parse(json);
247
+ }
248
+ /** Scratch-evaluate `formula` at `(rowIndex, columnIndex)` on `sheetId` — the cell editor's live
249
+ * result preview. Read-only: nothing is installed or committed, no dirty marks; a host
250
+ * custom-function call (`=AOP(...)`) evaluates to `#NAME?` with NO `customEvals` request (host
251
+ * calls are recognised at set-value time only), so previewing can never fire one. Returns the
252
+ * result as an `ExtendedValue`, or `undefined` when the backing engine can't evaluate it (no
253
+ * capability, parse failure, unknown sheet, unpopulated engine). */
254
+ evalFormula(sheetId, rowIndex, columnIndex, formula) {
255
+ if (!formula || !this.inner.evalFormula)
256
+ return undefined;
257
+ const json = this.inner.evalFormula(sheetId, rowIndex, columnIndex, formula);
258
+ const parsed = json ? JSON.parse(json) : null;
259
+ return parsed ?? undefined;
260
+ }
261
+ /** Distinct fill/font colors + CF icons present in one column's body rows — the FilterBox
262
+ * "Filter by color" swatches, computed engine-side in ONE column scan (the windowed
263
+ * alternative to the JS per-row `getEffectiveFormat` scan). `undefined` when the backing
264
+ * engine doesn't expose it. */
265
+ filterColors(sheetId, columnIndex, startRow, endRow) {
266
+ if (!this.inner.filterColors)
267
+ return undefined;
268
+ const json = this.inner.filterColors(sheetId, columnIndex, startRow, endRow);
269
+ if (!json || json === "null")
270
+ return undefined;
271
+ return JSON.parse(json);
272
+ }
273
+ /** Sheet ids, in order. */
274
+ sheetIds() {
275
+ return Array.from(this.inner.sheetIds());
276
+ }
277
+ /**
278
+ * Remove the host-seeded default `Sheet1` (sheetId 1) once a collab merge showed the workbook's
279
+ * real sheets live under other ids — without it, the seed's live structs ship with every outbound
280
+ * flush, materializing an empty ghost `Sheet1` tab for every collaborator (and re-materializing
281
+ * it after a peer's explicit delete-sheet). Call right after merging the collab doc, and ONLY
282
+ * when the persisted doc's sheet registry is non-empty and contains no sheetId 1; the store
283
+ * guards the rest (exact seed title, zero populated cells, ≥1 other sheet) and removes the seed
284
+ * WITHOUT a commit fan-out — the tombstones ride the next flushed update. No-op (returns false)
285
+ * on stores without a construction seed.
286
+ */
287
+ pruneDanglingDefaultSeed() {
288
+ return this.inner.pruneDanglingDefaultSeed?.() ?? false;
289
+ }
290
+ /** The workbook's sheets in tab order — the engine's AUTHORITATIVE identity/order/titles (it
291
+ * generates ids for `create-sheet`, rewrites titles on rename, reorders on move). A thin UI
292
+ * reconciles its tab list from this after each commit. Returns `[]` if the backing engine doesn't
293
+ * expose it (the hook then keeps its seeded sheet list). */
294
+ sheets() {
295
+ const json = this.inner.sheets?.();
296
+ if (json == null)
297
+ return [];
298
+ return JSON.parse(json);
299
+ }
300
+ /** Per-sheet 1-based data extent: `[sheetId, rowCount, columnCount][]`. A windowed renderer sizes
301
+ * the grid's scroll extent to this (so it holds only the visible viewport in JS yet scrolls the
302
+ * full sheet). Returns `[]` if the backing engine doesn't expose extents. */
303
+ sheetExtents() {
304
+ const json = this.inner.sheetExtents?.();
305
+ if (json == null)
306
+ return [];
307
+ return JSON.parse(json);
308
+ }
309
+ /** A sheet's display values as RFC-4180 CSV. Empty string if the backing engine can't export. */
310
+ exportCsv(sheetId) {
311
+ return this.inner.exportCsv?.(sheetId) ?? "";
312
+ }
313
+ /** The whole workbook as in-memory XLSX bytes (a `Uint8Array`), or `undefined` if the backing
314
+ * engine wasn't built with export support. Reads the full document inside the engine (works even in
315
+ * windowed mode, where the JS side holds only the viewport). */
316
+ exportXlsx() {
317
+ return this.inner.exportXlsx?.();
318
+ }
319
+ /**
320
+ * Import an `.xlsx` workbook with the native Rust reader (`rnc-import`), REPLACING the engine's
321
+ * document, and return the full {@link CommitResult} (every cell + style + structural change — the
322
+ * same shape as {@link load}). Returns `undefined` if the backend has no import capability.
323
+ *
324
+ * NOTE: unlike {@link load}, this does NOT notify {@link onChanges} subscribers — because import is a
325
+ * full replace, the caller must reset its render state and fold this commit onto sheets seeded from
326
+ * {@link sheets} (merges/dimensions fold only onto pre-existing sheet objects). The windowed hook's
327
+ * `importXlsx` does exactly that. Only backends whose commit shape matches {@link CommitResult}
328
+ * (rnc-engine's built-in engine) are driven here; the collab (yrs-store) engine imports via its delta.
329
+ */
330
+ importXlsx(bytes) {
331
+ if (!this.inner.importXlsx)
332
+ return undefined;
333
+ return JSON.parse(this.inner.importXlsx(bytes));
334
+ }
335
+ /** Raw JSON of {@link documentFacets} — the hook diffs this string against the previous commit's to
336
+ * skip a no-op facet update (so a pure cell edit doesn't churn the facet props and re-render the grid). */
337
+ documentFacetsJson() {
338
+ return this.inner.documentFacets();
339
+ }
340
+ /** The document-level facets the renderer draws (CF rules, tables, named/protected ranges, embeds,
341
+ * slicers, banded ranges, basic filters, DV rules) in their canonical shapes. Cheap and re-read by
342
+ * the thin hook once per commit — they're small and change rarely, so they ride this getter rather
343
+ * than a per-facet `CommitResult` channel. */
344
+ documentFacets() {
345
+ return JSON.parse(this.documentFacetsJson());
346
+ }
347
+ /** Register a host (JS) custom-function NAME so `=NAME(…)` resolves (deps tracked) instead of
348
+ * `#NAME?`. On recalc the engine emits a `customEvals` request the host answers via
349
+ * `setEffectiveValue`. (The function body itself lives in JS — see the hook's
350
+ * `registerCustomFunction`, which pairs this with the resolve loop.) */
351
+ registerCustomFunction(name) {
352
+ this.inner.registerCustomFunction(name);
353
+ }
354
+ /** The PRECEDENTS of a cell — the cells + ranges its formula references (engine-parsed, exact). The
355
+ * engine owns the dependency data, so `getPrecedents` is backed by real graph data, not a stub. */
356
+ precedents(sheetId, rowIndex, columnIndex) {
357
+ return JSON.parse(this.inner.precedents(sheetId, rowIndex, columnIndex));
358
+ }
359
+ /** The DEPENDENTS of a cell — every formula cell that references it (directly or within a range). */
360
+ dependents(sheetId, rowIndex, columnIndex) {
361
+ return JSON.parse(this.inner.dependents(sheetId, rowIndex, columnIndex));
362
+ }
363
+ // ── Yjs sync (collab engines only) ────────────────────────────────────────────────────────
364
+ // Present when the injected engine implements {@link CollabSync} (i.e. `CollabSpreadsheetEngine`).
365
+ // The host bridges these to a `Y.Doc`: feed remote peer/yhub updates in via `applyRemoteUpdate`,
366
+ // and push local edits out by applying `encodeDiffV1(remoteStateVector)` to the doc after a commit.
367
+ /** Apply a remote Yjs v1 update (a peer's / yhub's delta) into the CRDT doc, rebuild, and fold the
368
+ * resulting change-set to {@link onChanges} subscribers. Returns it inline too. Throws if the
369
+ * backing engine isn't collaborative ({@link isCollab}). */
370
+ applyRemoteUpdate(update) {
371
+ const collab = this.collab();
372
+ collab.applyUpdate(update);
373
+ const commit = JSON.parse(collab.syncRemote());
374
+ this.listeners.forEach((listener) => listener(commit));
375
+ return commit;
376
+ }
377
+ /** Encode the full document state as a Yjs v1 update (initial sync to a peer / persistence). */
378
+ encodeStateAsUpdate() {
379
+ return this.collab().encodeStateAsUpdate();
380
+ }
381
+ /** Encode this peer's Yjs state vector (a compact summary of what it has seen). */
382
+ encodeStateVector() {
383
+ return this.collab().encodeStateVector();
384
+ }
385
+ /** Encode the diff a remote peer (identified by its state vector) is missing — apply the result to
386
+ * the transport doc (`Y.applyUpdate`) to propagate local edits. */
387
+ encodeDiffV1(remoteStateVector) {
388
+ return this.collab().encodeDiffV1(remoteStateVector);
389
+ }
390
+ /** Release the underlying wasm memory. */
391
+ free() {
392
+ this.inner.free();
393
+ }
394
+ /** Fold `StyleEntry[]` into the consolidated `Map<"sheetId!A1", CellFormat>` the UI consumes.
395
+ * A `null` format clears the entry. */
396
+ static toStyleMap(styles) {
397
+ const map = new Map();
398
+ for (const style of styles) {
399
+ const key = cellKey(style.sheetId, style.rowIndex, style.columnIndex);
400
+ if (style.format)
401
+ map.set(key, style.format);
402
+ else
403
+ map.delete(key);
404
+ }
405
+ return map;
406
+ }
407
+ }
408
+ // Re-export the collaborative wasm engine so consumers can inject it into the hook:
409
+ // `useSpreadsheetEngine({ createEngine: () => new CollabSpreadsheetEngine() })`. The single-user
410
+ // `SpreadsheetEngine` is the default backend, so it stays internal (no export needed).
411
+ export { CollabSpreadsheetEngine };
412
+ // Cell-address key helpers live in a wasm-free module so the pure fold reducers can share them.
413
+ export { cellKey, columnLabel, parseCellKey } from "./keys";
414
+ // The Option B "UI is a renderer" surface: pure fold reducers + the engine-backed React hook.
415
+ export * from "./fold";
416
+ export { useEngineCore, } from "./use-engine-core";
417
+ export { useSpreadsheetUI, } from "./use-spreadsheet-ui";
418
+ export { useSpreadsheetEngine, } from "./use-spreadsheet-engine";
419
+ // The pivot-settings-sidebar bridge: drives `@rowsncolumns/pivot`'s `<PivotEditor>` with pure
420
+ // spec transforms over `update-pivot-table` (the engine re-renders on every update).
421
+ export { useEnginePivotEditor, VALUES_PIVOT_FIELD, } from "./use-engine-pivot-editor";
422
+ // File ⇄ engine I/O: bridge the `@rowsncolumns/toolkit` XLSX/ODS/CSV importer+exporter to the
423
+ // engine (no Rust changes). Import → `engine.load`/NDJSON stream; export → windowed `readWindow` →
424
+ // `createExcelFile`/`createODSFile`; CSV export passes through `engine.exportCsv`.
425
+ export { parseWorkbook, importXlsx, importOds, importCsv, streamWorkbookInto, exportXlsx, exportOds, exportCsv, } from "./io";
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `@rowsncolumns/rnc-engine/io` — file ⇄ engine bridge.
3
+ *
4
+ * Wires the existing `@rowsncolumns/toolkit` XLSX / ODS / CSV importer + exporter to {@link RncEngine}
5
+ * with NO Rust changes: both sides already speak the same `{ sheets, sheetData, sharedStrings, … }`
6
+ * `rnc-model` shape (`SheetData<T>` == `LoadDocument.sheetData`), so this is pure TS wiring.
7
+ *
8
+ * ── Import ────────────────────────────────────────────────────────────────────────────────────
9
+ * `importXlsx` / `importOds` drive the toolkit's `WorkBook` plugin DIRECTLY (it does the parsing —
10
+ * the published `./worker` entry is just a Web-Worker wrapper around it, unavailable in Node). Its
11
+ * chunked `processSheetData()` rows are assembled into `SheetData` and fed to the engine. For very
12
+ * large inputs `streamWorkbookInto` emits one `{"t":"cell",…}` NDJSON record per cell into the
13
+ * engine's `streamBegin/streamWrite/streamEnd`, so the workbook never lands whole in the engine in
14
+ * one `loadDocument` call. CSV uses the toolkit's `createRowDataFromCSVString` → `engine.load`.
15
+ *
16
+ * ── Export ────────────────────────────────────────────────────────────────────────────────────
17
+ * `exportXlsx` / `exportOds` read the engine back: `documentFacets()` for the document facets,
18
+ * `sheetExtents()` for each sheet's used 1-based extent, and `readWindow` over that extent (in row
19
+ * bands) for the cell VALUES + consolidated styles, which are assembled into the toolkit's export
20
+ * input and handed to `createExcelFile` / `createODSFile`. CSV is direct: `engine.exportCsv`.
21
+ *
22
+ * CONSTRAINT (engine surface): `readWindow` returns the value-side render data only
23
+ * (`{ rowIndex, columnIndex, formatted, number, isFormula }`) plus consolidated styles — it does
24
+ * NOT carry a cell's verbatim `CellData`, so the FORMULA SOURCE TEXT of a `=…` cell is not
25
+ * recoverable. Exported cells therefore carry their COMPUTED value (number / formatted string) +
26
+ * style, exactly like the engine's own CSV export. This is the same lossy boundary `exportCsv` has;
27
+ * a lossless formula export would need a new engine reader (out of scope — no Rust changes).
28
+ */
29
+ import type { CellData, CommentThread, SheetData } from "@rowsncolumns/common-types";
30
+ import type { Sheet } from "@rowsncolumns/spreadsheet";
31
+ import { RncEngine, type LoadDocument } from "./index";
32
+ /** A loaded workbook in the `rnc-model` shape both the toolkit and the engine speak. */
33
+ export interface ParsedWorkbook {
34
+ sheets: Sheet[];
35
+ sheetData: SheetData<CellData>;
36
+ /** Shared-string table keyed by its string index (cells reference it via `cell.ss`). */
37
+ sharedStrings: Map<string, string>;
38
+ tables: LoadDocument["tables"];
39
+ conditionalFormats: LoadDocument["conditionalFormats"];
40
+ dataValidations: LoadDocument["dataValidations"];
41
+ namedRanges: LoadDocument["namedRanges"];
42
+ /**
43
+ * Threaded comments in the exporter's `comments` model. The engine only
44
+ * stores the per-cell `commentThreadId` anchors (thread CONTENT is
45
+ * host-managed state, exactly as `useSpreadsheetState` models it), so
46
+ * hosts must persist this list themselves to render / re-export threads.
47
+ */
48
+ commentThreads: CommentThread[];
49
+ }
50
+ /** Options shared by the file-importing entry points. */
51
+ export interface ImportOptions {
52
+ /** BCP-47 locale used when the parser infers value types (e.g. CSV `3/3/2022` → date serial). */
53
+ locale?: string;
54
+ /** Minimum grid rows/cols to request from the workbook plugin (passed to `getSheets`). */
55
+ minRowCount?: number;
56
+ minColumnCount?: number;
57
+ }
58
+ /**
59
+ * Parse an XLSX (or any format the toolkit's `WorkBook` plugin registry handles for the given file
60
+ * name — `.xlsx` / `.xlsm` …) buffer into the common `rnc-model` shape, WITHOUT touching the engine.
61
+ * Drives the toolkit `WorkBook` directly (Node-safe: it uses `@zip.js/zip.js` + `fast-xml-parser`,
62
+ * no browser/Worker APIs). Shared-string cells (which the plugin emits as a bare `ss` index) are
63
+ * resolved inline to their value via {@link resolveSharedStrings}, since the engine has no SST table.
64
+ */
65
+ export declare function parseWorkbook(buffer: ArrayBuffer, fileName?: string, options?: ImportOptions): Promise<ParsedWorkbook>;
66
+ /**
67
+ * Import an XLSX buffer into the engine via the toolkit. Returns the engine the cells were loaded
68
+ * into (a fresh one when `engine` is omitted). Hydrates through {@link RncEngine.load}, so it
69
+ * delivers the full change-set to `onChanges` subscribers like every other load.
70
+ */
71
+ export declare function importXlsx(buffer: ArrayBuffer, options?: ImportOptions & {
72
+ engine?: RncEngine;
73
+ fileName?: string;
74
+ }): Promise<RncEngine>;
75
+ /** Import an ODS buffer into the engine. Same path as {@link importXlsx} with an `.ods` file name so
76
+ * the toolkit selects its ODS plugin. */
77
+ export declare function importOds(buffer: ArrayBuffer, options?: ImportOptions & {
78
+ engine?: RncEngine;
79
+ fileName?: string;
80
+ }): Promise<RncEngine>;
81
+ /**
82
+ * Stream a parsed workbook's cells into the engine's NDJSON hydration path instead of one big
83
+ * `loadDocument`, so a huge workbook is fed cell-by-cell (one `{"t":"cell",…}` record each) and never
84
+ * materialized whole on the engine side in a single call. The sheets are registered first (as
85
+ * `{"t":"sheet",…}` records). Pull the visible area with `readWindow` afterward.
86
+ *
87
+ * NOTE: document facets (CF / DV / tables / named ranges) are NOT carried by the NDJSON stream — load
88
+ * those separately if needed. Use this for value-heavy bulk imports; use {@link importXlsx} for the
89
+ * full-fidelity (facets included) path.
90
+ */
91
+ export declare function streamWorkbookInto(engine: RncEngine, parsed: ParsedWorkbook): void;
92
+ /**
93
+ * Import a CSV string (or `File`) into the engine via the toolkit's CSV parser. The CSV becomes a
94
+ * single sheet (`sheetId` / `title`, both defaultable). Returns the engine.
95
+ */
96
+ export declare function importCsv(csv: string | File, options?: ImportOptions & {
97
+ engine?: RncEngine;
98
+ sheetId?: number;
99
+ title?: string;
100
+ }): Promise<RncEngine>;
101
+ /**
102
+ * Export the engine's contents to an XLSX file (a `Uint8Array` of the .xlsx zip) via the toolkit's
103
+ * `createExcelFile`. Reads VALUES + styles from the engine (see the formula-source caveat in the file
104
+ * header). `bandRows` controls the row-band height of the windowed reads.
105
+ */
106
+ export declare function exportXlsx(engine: RncEngine, options?: {
107
+ bandRows?: number;
108
+ }): Promise<Uint8Array>;
109
+ /** Export the engine's contents to an ODS file (a `Uint8Array`) via the toolkit's `createODSFile`
110
+ * (which returns a raw `ArrayBuffer` — wrapped here so both exporters return a `Uint8Array`). */
111
+ export declare function exportOds(engine: RncEngine, options?: {
112
+ bandRows?: number;
113
+ }): Promise<Uint8Array>;
114
+ /**
115
+ * Export a sheet's display values as RFC-4180 CSV — a thin pass-through to the engine's native
116
+ * `exportCsv` (already a direct capability; re-exported here so all file I/O lives in one module).
117
+ */
118
+ export declare function exportCsv(engine: RncEngine, sheetId: number): string;
119
+ //# sourceMappingURL=io.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,aAAa,EAEb,SAAS,EACV,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAQvD,OAAO,EAAE,SAAS,EAAuB,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5E,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC/B,wFAAwF;IACxF,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC/B,kBAAkB,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAC;IACvD,eAAe,EAAE,YAAY,CAAC,iBAAiB,CAAC,CAAC;IACjD,WAAW,EAAE,YAAY,CAAC,aAAa,CAAC,CAAC;IACzC;;;;;OAKG;IACH,cAAc,EAAE,aAAa,EAAE,CAAC;CACjC;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAmCD;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,QAAQ,SAAkB,EAC1B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,cAAc,CAAC,CAqDzB;AAyBD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,aAAa,GAAG;IAAE,MAAM,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACtE,OAAO,CAAC,SAAS,CAAC,CAUpB;AAED;yCACyC;AACzC,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,aAAa,GAAG;IAAE,MAAM,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACtE,OAAO,CAAC,SAAS,CAAC,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,GACrB,IAAI,CA0CN;AA6CD;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,OAAO,GAAE,aAAa,GAAG;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACX,GACL,OAAO,CAAC,SAAS,CAAC,CAoBpB;AAsFD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,UAAU,CAAC,CAGrB;AAED;iGACiG;AACjG,wBAAsB,SAAS,CAC7B,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,UAAU,CAAC,CAGrB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpE"}