@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.
- package/LICENSE.md +100 -0
- package/README.md +83 -0
- package/dist/esm/command-handlers.d.ts +169 -0
- package/dist/esm/command-handlers.d.ts.map +1 -0
- package/dist/esm/command-handlers.js +404 -0
- package/dist/esm/fold.d.ts +140 -0
- package/dist/esm/fold.d.ts.map +1 -0
- package/dist/esm/fold.js +539 -0
- package/dist/esm/index.d.ts +620 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +425 -0
- package/dist/esm/io.d.ts +119 -0
- package/dist/esm/io.d.ts.map +1 -0
- package/dist/esm/io.js +318 -0
- package/dist/esm/keys.d.ts +20 -0
- package/dist/esm/keys.d.ts.map +1 -0
- package/dist/esm/keys.js +41 -0
- package/dist/esm/move-rescope.d.ts +18 -0
- package/dist/esm/move-rescope.d.ts.map +1 -0
- package/dist/esm/move-rescope.js +38 -0
- package/dist/esm/use-engine-core.d.ts +293 -0
- package/dist/esm/use-engine-core.d.ts.map +1 -0
- package/dist/esm/use-engine-core.js +2279 -0
- package/dist/esm/use-engine-pivot-editor.d.ts +78 -0
- package/dist/esm/use-engine-pivot-editor.d.ts.map +1 -0
- package/dist/esm/use-engine-pivot-editor.js +252 -0
- package/dist/esm/use-spreadsheet-engine.d.ts +7 -0
- package/dist/esm/use-spreadsheet-engine.d.ts.map +1 -0
- package/dist/esm/use-spreadsheet-engine.js +23 -0
- package/dist/esm/use-spreadsheet-ui.d.ts +39 -0
- package/dist/esm/use-spreadsheet-ui.d.ts.map +1 -0
- package/dist/esm/use-spreadsheet-ui.js +78 -0
- package/dist/esm/wasm/rnc_wasm.d.ts +394 -0
- package/dist/esm/wasm/rnc_wasm.js +1455 -0
- package/dist/esm/wasm/rnc_wasm_bg.wasm +0 -0
- package/dist/esm/wasm/rnc_wasm_bg.wasm.d.ts +81 -0
- 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";
|
package/dist/esm/io.d.ts
ADDED
|
@@ -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"}
|