@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
package/dist/esm/io.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { WorkBook, createExcelFile, createODSFile, createRowDataFromCSVString, } from "@rowsncolumns/toolkit";
|
|
2
|
+
import { RncEngine } from "./index";
|
|
3
|
+
const PROCESS_CHUNK_ROWS = 1000;
|
|
4
|
+
/**
|
|
5
|
+
* Inline-resolve every shared-string cell into a concrete value. The toolkit's XLSX parser emits a
|
|
6
|
+
* text cell as `{ ss: "<index>" }` with NO value field (the string lives only in the SST), but the
|
|
7
|
+
* engine has no shared-string table — so it would store such a cell empty. For each cell carrying an
|
|
8
|
+
* `ss` that resolves in `sharedStrings`, set `ue.sv` / `ev.sv` / `fv` to the string (mirrors
|
|
9
|
+
* `SheetCell.hydrateSharedStringValue`), then drop the now-redundant `ss`. Cells that already carry a
|
|
10
|
+
* value (numbers, formulas) are untouched. Mutates `sheetData` in place.
|
|
11
|
+
*/
|
|
12
|
+
function resolveSharedStrings(sheetData, sharedStrings) {
|
|
13
|
+
if (sharedStrings.size === 0)
|
|
14
|
+
return;
|
|
15
|
+
for (const rows of Object.values(sheetData)) {
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
const values = row?.values;
|
|
18
|
+
if (!values)
|
|
19
|
+
continue;
|
|
20
|
+
for (let c = 0; c < values.length; c++) {
|
|
21
|
+
const cell = values[c];
|
|
22
|
+
if (cell?.ss == null)
|
|
23
|
+
continue;
|
|
24
|
+
const text = sharedStrings.get(String(cell.ss));
|
|
25
|
+
if (text === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
cell.ue = { ...cell.ue, sv: text };
|
|
28
|
+
cell.ev = { ...cell.ev, sv: text };
|
|
29
|
+
cell.fv ??= text;
|
|
30
|
+
delete cell.ss;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse an XLSX (or any format the toolkit's `WorkBook` plugin registry handles for the given file
|
|
37
|
+
* name — `.xlsx` / `.xlsm` …) buffer into the common `rnc-model` shape, WITHOUT touching the engine.
|
|
38
|
+
* Drives the toolkit `WorkBook` directly (Node-safe: it uses `@zip.js/zip.js` + `fast-xml-parser`,
|
|
39
|
+
* no browser/Worker APIs). Shared-string cells (which the plugin emits as a bare `ss` index) are
|
|
40
|
+
* resolved inline to their value via {@link resolveSharedStrings}, since the engine has no SST table.
|
|
41
|
+
*/
|
|
42
|
+
export async function parseWorkbook(buffer, fileName = "workbook.xlsx", options = {}) {
|
|
43
|
+
const workbook = new WorkBook();
|
|
44
|
+
await workbook.load(buffer, fileName);
|
|
45
|
+
try {
|
|
46
|
+
const [sheets, tables] = await workbook.getSheets(options.minRowCount, options.minColumnCount);
|
|
47
|
+
// Assemble SheetData from the chunked generator: each chunk's `rows[i]` belongs at
|
|
48
|
+
// `sheetData[sheetId][rowIndices[i]]` (the same placement the import hook uses).
|
|
49
|
+
const sheetData = {};
|
|
50
|
+
for await (const chunk of workbook.processSheetData(PROCESS_CHUNK_ROWS, true, true)) {
|
|
51
|
+
const rows = (sheetData[chunk.sheetId] ??= []);
|
|
52
|
+
for (let i = 0; i < chunk.rowIndices.length; i++) {
|
|
53
|
+
rows[chunk.rowIndices[i]] = chunk.rows[i];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// The plugin's SST is a `string[]` keyed by index; cells reference it by that index in `cell.ss`.
|
|
57
|
+
const sharedStrings = new Map();
|
|
58
|
+
(workbook.sharedStrings ?? []).forEach((value, index) => {
|
|
59
|
+
sharedStrings.set(String(index), value);
|
|
60
|
+
});
|
|
61
|
+
// A shared-string cell from the plugin carries ONLY `ss` (its SST index) — no value field. The
|
|
62
|
+
// engine has no shared-string table (and we make no Rust changes), so resolve each `ss` into an
|
|
63
|
+
// inline `ue.sv`/`ev.sv`/`fv` here, mirroring `SheetCell.hydrateSharedStringValue`. Otherwise the
|
|
64
|
+
// engine would store text cells with no value and `readWindow` would return them empty.
|
|
65
|
+
resolveSharedStrings(sheetData, sharedStrings);
|
|
66
|
+
const [conditionalFormats, dataValidations] = await Promise.all([
|
|
67
|
+
workbook.getConditionalFormatting(),
|
|
68
|
+
workbook.getDataValidations(),
|
|
69
|
+
]);
|
|
70
|
+
return {
|
|
71
|
+
sheets,
|
|
72
|
+
sheetData,
|
|
73
|
+
sharedStrings,
|
|
74
|
+
tables,
|
|
75
|
+
conditionalFormats,
|
|
76
|
+
dataValidations,
|
|
77
|
+
namedRanges: workbook.getNamedRanges(),
|
|
78
|
+
commentThreads: await workbook.getCommentThreads(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
workbook.dispose();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Map a {@link ParsedWorkbook} to the engine's {@link LoadDocument} hydration shape. */
|
|
86
|
+
function toLoadDocument(parsed) {
|
|
87
|
+
return {
|
|
88
|
+
sheets: parsed.sheets.map((sheet) => ({
|
|
89
|
+
sheetId: sheet.sheetId,
|
|
90
|
+
title: sheet.title,
|
|
91
|
+
})),
|
|
92
|
+
sheetData: parsed.sheetData,
|
|
93
|
+
conditionalFormats: parsed.conditionalFormats,
|
|
94
|
+
dataValidations: parsed.dataValidations,
|
|
95
|
+
tables: parsed.tables,
|
|
96
|
+
namedRanges: parsed.namedRanges,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Register every parsed sheet on the engine that isn't already present. */
|
|
100
|
+
function ensureSheets(engine, parsed) {
|
|
101
|
+
const existing = new Set(engine.sheetIds());
|
|
102
|
+
for (const sheet of parsed.sheets) {
|
|
103
|
+
if (!existing.has(sheet.sheetId))
|
|
104
|
+
engine.addSheet(sheet.sheetId, sheet.title);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Import an XLSX buffer into the engine via the toolkit. Returns the engine the cells were loaded
|
|
109
|
+
* into (a fresh one when `engine` is omitted). Hydrates through {@link RncEngine.load}, so it
|
|
110
|
+
* delivers the full change-set to `onChanges` subscribers like every other load.
|
|
111
|
+
*/
|
|
112
|
+
export async function importXlsx(buffer, options = {}) {
|
|
113
|
+
const parsed = await parseWorkbook(buffer, options.fileName ?? "workbook.xlsx", options);
|
|
114
|
+
const engine = options.engine ?? (await RncEngine.create());
|
|
115
|
+
ensureSheets(engine, parsed);
|
|
116
|
+
engine.load(toLoadDocument(parsed));
|
|
117
|
+
return engine;
|
|
118
|
+
}
|
|
119
|
+
/** Import an ODS buffer into the engine. Same path as {@link importXlsx} with an `.ods` file name so
|
|
120
|
+
* the toolkit selects its ODS plugin. */
|
|
121
|
+
export async function importOds(buffer, options = {}) {
|
|
122
|
+
return importXlsx(buffer, {
|
|
123
|
+
...options,
|
|
124
|
+
fileName: options.fileName ?? "workbook.ods",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Stream a parsed workbook's cells into the engine's NDJSON hydration path instead of one big
|
|
129
|
+
* `loadDocument`, so a huge workbook is fed cell-by-cell (one `{"t":"cell",…}` record each) and never
|
|
130
|
+
* materialized whole on the engine side in a single call. The sheets are registered first (as
|
|
131
|
+
* `{"t":"sheet",…}` records). Pull the visible area with `readWindow` afterward.
|
|
132
|
+
*
|
|
133
|
+
* NOTE: document facets (CF / DV / tables / named ranges) are NOT carried by the NDJSON stream — load
|
|
134
|
+
* those separately if needed. Use this for value-heavy bulk imports; use {@link importXlsx} for the
|
|
135
|
+
* full-fidelity (facets included) path.
|
|
136
|
+
*/
|
|
137
|
+
export function streamWorkbookInto(engine, parsed) {
|
|
138
|
+
const encoder = new TextEncoder();
|
|
139
|
+
engine.streamBegin();
|
|
140
|
+
try {
|
|
141
|
+
for (const sheet of parsed.sheets) {
|
|
142
|
+
engine.streamWrite(encoder.encode(JSON.stringify({
|
|
143
|
+
t: "sheet",
|
|
144
|
+
sheetId: sheet.sheetId,
|
|
145
|
+
title: sheet.title,
|
|
146
|
+
}) + "\n"));
|
|
147
|
+
}
|
|
148
|
+
for (const [sheetIdKey, rows] of Object.entries(parsed.sheetData)) {
|
|
149
|
+
const sheetId = Number(sheetIdKey);
|
|
150
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
151
|
+
const values = rows[rowIndex]?.values;
|
|
152
|
+
if (!values)
|
|
153
|
+
continue;
|
|
154
|
+
for (let columnIndex = 0; columnIndex < values.length; columnIndex++) {
|
|
155
|
+
const cell = values[columnIndex];
|
|
156
|
+
if (cell == null)
|
|
157
|
+
continue;
|
|
158
|
+
// `ue`/`uf`/`ev`/`fv`/`ss` are flattened onto the record alongside the `t`/coords (the
|
|
159
|
+
// engine's `StreamRecord` `#[serde(flatten)] cell: CellData`).
|
|
160
|
+
engine.streamWrite(encoder.encode(JSON.stringify({
|
|
161
|
+
t: "cell",
|
|
162
|
+
sheetId,
|
|
163
|
+
rowIndex,
|
|
164
|
+
columnIndex,
|
|
165
|
+
...cell,
|
|
166
|
+
}) + "\n"));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
engine.streamEnd();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Promote a CSV cell's INFERRED type into its user-entered value. The toolkit's CSV parser puts the
|
|
177
|
+
* raw text in `ue.sv` and the type-inferred value (e.g. `42`) in the EFFECTIVE value `ev.nv`/`ev.bv`.
|
|
178
|
+
* The engine, however, re-derives the effective value from `ue` on load and takes `ue.sv` verbatim as
|
|
179
|
+
* TEXT (it does not re-parse numeric strings, and the supplied `ev` is overwritten by recompute) — so
|
|
180
|
+
* an inferred number would come back as a string. Copy the inferred `nv`/`bv` onto `ue` so the engine
|
|
181
|
+
* stores it as the right type. Returns the cell unchanged when nothing was inferred.
|
|
182
|
+
*/
|
|
183
|
+
function csvCellToTypedInput(cell) {
|
|
184
|
+
const inferred = cell.ev;
|
|
185
|
+
if (!inferred || cell.ue?.nv != null || cell.ue?.fv != null)
|
|
186
|
+
return cell;
|
|
187
|
+
if (inferred.nv != null)
|
|
188
|
+
return { ...cell, ue: { ...cell.ue, nv: inferred.nv } };
|
|
189
|
+
if (inferred.bv != null)
|
|
190
|
+
return { ...cell, ue: { ...cell.ue, bv: inferred.bv } };
|
|
191
|
+
return cell;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Shift a 0-indexed `RowData[]` (rows AND columns from 0) into the engine's 1-indexed `SheetData`
|
|
195
|
+
* row list (index 0 reserved): row `i` moves to slot `i + 1`, and each cell at column `j` moves to
|
|
196
|
+
* `values[j + 1]`. The toolkit's CSV parser is the only 0-indexed producer here (the XLSX plugin is
|
|
197
|
+
* already 1-indexed), so this is CSV-only. Each cell's inferred type is promoted onto `ue` via
|
|
198
|
+
* {@link csvCellToTypedInput} so the engine stores numbers as numbers, not text.
|
|
199
|
+
*/
|
|
200
|
+
function toOneIndexed(rowData) {
|
|
201
|
+
const out = [null]; // index 0 reserved
|
|
202
|
+
for (const row of rowData) {
|
|
203
|
+
const src = row?.values;
|
|
204
|
+
if (!src) {
|
|
205
|
+
out.push(null);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const shifted = [null];
|
|
209
|
+
for (let c = 0; c < src.length; c++) {
|
|
210
|
+
const cell = src[c];
|
|
211
|
+
shifted[c + 1] = cell ? csvCellToTypedInput(cell) : null;
|
|
212
|
+
}
|
|
213
|
+
out.push({ values: shifted });
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Import a CSV string (or `File`) into the engine via the toolkit's CSV parser. The CSV becomes a
|
|
219
|
+
* single sheet (`sheetId` / `title`, both defaultable). Returns the engine.
|
|
220
|
+
*/
|
|
221
|
+
export async function importCsv(csv, options = {}) {
|
|
222
|
+
const sheetId = options.sheetId ?? 1;
|
|
223
|
+
const title = options.title ?? "Sheet1";
|
|
224
|
+
// The toolkit's CSV parser emits 0-INDEXED rows AND columns (`rowData[i].values[j]` for the cell at
|
|
225
|
+
// row i / col j, both starting at 0). The engine's grid is 1-INDEXED with array index 0 reserved, so
|
|
226
|
+
// shift both axes up by one before loading (CSV row 0 → engine row 1, column 0 → engine column 1).
|
|
227
|
+
const rowData = await createRowDataFromCSVString(csv, true, options.locale);
|
|
228
|
+
const sheetData = { [sheetId]: toOneIndexed(rowData) };
|
|
229
|
+
const engine = options.engine ?? (await RncEngine.create());
|
|
230
|
+
const existing = new Set(engine.sheetIds());
|
|
231
|
+
if (!existing.has(sheetId))
|
|
232
|
+
engine.addSheet(sheetId, title);
|
|
233
|
+
engine.load({
|
|
234
|
+
sheets: [{ sheetId, title }],
|
|
235
|
+
sheetData,
|
|
236
|
+
});
|
|
237
|
+
return engine;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Build the toolkit-export `{ sheets, sheetData, … }` input by reading the engine back with
|
|
241
|
+
* `documentFacets` + `sheetExtents` + windowed `readWindow` reads (in `bandRows`-tall bands). Cell
|
|
242
|
+
* values come from the window (`number` → `ue.nv`, else `formatted` → `ue.sv`) and consolidated
|
|
243
|
+
* styles are folded onto each cell's `uf`. See the file header for the formula-source caveat.
|
|
244
|
+
*/
|
|
245
|
+
function readEngineForExport(engine, bandRows = 5000) {
|
|
246
|
+
const facets = engine.documentFacets();
|
|
247
|
+
const extents = new Map(engine
|
|
248
|
+
.sheetExtents()
|
|
249
|
+
.map(([sheetId, rows, cols]) => [sheetId, { rows, cols }]));
|
|
250
|
+
const sheets = [];
|
|
251
|
+
const sheetData = {};
|
|
252
|
+
for (const sheetId of engine.sheetIds()) {
|
|
253
|
+
sheets.push({ sheetId, title: `Sheet${sheetId}` });
|
|
254
|
+
const extent = extents.get(sheetId);
|
|
255
|
+
if (!extent || extent.rows < 1 || extent.cols < 1) {
|
|
256
|
+
sheetData[sheetId] = [];
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const rows = [];
|
|
260
|
+
const rowAt = (rowIndex) => (rows[rowIndex] ??= { values: [] });
|
|
261
|
+
for (let bandStart = 1; bandStart <= extent.rows; bandStart += bandRows) {
|
|
262
|
+
const bandEnd = Math.min(bandStart + bandRows - 1, extent.rows);
|
|
263
|
+
const snapshot = engine.readWindow(sheetId, bandStart, 1, bandEnd, extent.cols);
|
|
264
|
+
for (const cell of snapshot.cells) {
|
|
265
|
+
const cellData = cell.number != null
|
|
266
|
+
? {
|
|
267
|
+
ue: { nv: cell.number },
|
|
268
|
+
ev: { nv: cell.number },
|
|
269
|
+
fv: cell.formatted,
|
|
270
|
+
}
|
|
271
|
+
: {
|
|
272
|
+
ue: { sv: cell.formatted },
|
|
273
|
+
ev: { sv: cell.formatted },
|
|
274
|
+
fv: cell.formatted,
|
|
275
|
+
};
|
|
276
|
+
rowAt(cell.rowIndex).values[cell.columnIndex] = cellData;
|
|
277
|
+
}
|
|
278
|
+
// Fold consolidated styles onto the matching cell's `uf` (the format the exporter reads).
|
|
279
|
+
for (const style of snapshot.styles) {
|
|
280
|
+
if (!style.format)
|
|
281
|
+
continue;
|
|
282
|
+
const values = rowAt(style.rowIndex).values;
|
|
283
|
+
const cellData = (values[style.columnIndex] ??= {});
|
|
284
|
+
cellData.uf = style.format;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
sheetData[sheetId] = rows;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
sheets,
|
|
291
|
+
sheetData,
|
|
292
|
+
tables: facets.tables,
|
|
293
|
+
conditionalFormats: facets.conditionalFormats,
|
|
294
|
+
namedRanges: facets.namedRanges,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Export the engine's contents to an XLSX file (a `Uint8Array` of the .xlsx zip) via the toolkit's
|
|
299
|
+
* `createExcelFile`. Reads VALUES + styles from the engine (see the formula-source caveat in the file
|
|
300
|
+
* header). `bandRows` controls the row-band height of the windowed reads.
|
|
301
|
+
*/
|
|
302
|
+
export async function exportXlsx(engine, options = {}) {
|
|
303
|
+
const input = readEngineForExport(engine, options.bandRows);
|
|
304
|
+
return createExcelFile(input);
|
|
305
|
+
}
|
|
306
|
+
/** Export the engine's contents to an ODS file (a `Uint8Array`) via the toolkit's `createODSFile`
|
|
307
|
+
* (which returns a raw `ArrayBuffer` — wrapped here so both exporters return a `Uint8Array`). */
|
|
308
|
+
export async function exportOds(engine, options = {}) {
|
|
309
|
+
const input = readEngineForExport(engine, options.bandRows);
|
|
310
|
+
return new Uint8Array(await createODSFile(input));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Export a sheet's display values as RFC-4180 CSV — a thin pass-through to the engine's native
|
|
314
|
+
* `exportCsv` (already a direct capability; re-exported here so all file I/O lives in one module).
|
|
315
|
+
*/
|
|
316
|
+
export function exportCsv(engine, sheetId) {
|
|
317
|
+
return engine.exportCsv(sheetId);
|
|
318
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-address key helpers — wasm-free so both the engine wrapper and the pure fold reducers can use
|
|
3
|
+
* them without pulling in the wasm module. The consolidated style-map key MUST match the UI's
|
|
4
|
+
* `useSheetProperties` `getEffectiveFormat` lookup (`"<sheetId>!<A1>"`).
|
|
5
|
+
*/
|
|
6
|
+
/** 1-indexed column number → A1 column label (`1` → `"A"`, `27` → `"AA"`). */
|
|
7
|
+
export declare function columnLabel(column: number): string;
|
|
8
|
+
/** Consolidated style-map key: `"<sheetId>!<A1>"`, e.g. `(1, 3, 2)` → `"1!B3"`. */
|
|
9
|
+
export declare function cellKey(sheetId: number, row: number, column: number): string;
|
|
10
|
+
/** 1-based column number for an A1 column label (`"A"` → 1, `"AA"` → 27). Inverse of
|
|
11
|
+
* {@link columnLabel}. */
|
|
12
|
+
export declare function columnNumberFromLabel(label: string): number;
|
|
13
|
+
/** Inverse of {@link cellKey}: `"1!B3"` → `{ sheetId: 1, rowIndex: 3, columnIndex: 2 }`. Returns
|
|
14
|
+
* `null` for anything that isn't a well-formed key, so callers can skip it rather than guess. */
|
|
15
|
+
export declare function parseCellKey(key: string): {
|
|
16
|
+
sheetId: number;
|
|
17
|
+
rowIndex: number;
|
|
18
|
+
columnIndex: number;
|
|
19
|
+
} | null;
|
|
20
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/keys.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD;AAED,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED;0BAC0B;AAC1B,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED;iGACiG;AACjG,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,GACV;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQnE"}
|
package/dist/esm/keys.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell-address key helpers — wasm-free so both the engine wrapper and the pure fold reducers can use
|
|
3
|
+
* them without pulling in the wasm module. The consolidated style-map key MUST match the UI's
|
|
4
|
+
* `useSheetProperties` `getEffectiveFormat` lookup (`"<sheetId>!<A1>"`).
|
|
5
|
+
*/
|
|
6
|
+
/** 1-indexed column number → A1 column label (`1` → `"A"`, `27` → `"AA"`). */
|
|
7
|
+
export function columnLabel(column) {
|
|
8
|
+
let label = "";
|
|
9
|
+
let n = column;
|
|
10
|
+
while (n > 0) {
|
|
11
|
+
const rem = (n - 1) % 26;
|
|
12
|
+
label = String.fromCharCode(65 + rem) + label;
|
|
13
|
+
n = Math.floor((n - 1) / 26);
|
|
14
|
+
}
|
|
15
|
+
return label;
|
|
16
|
+
}
|
|
17
|
+
/** Consolidated style-map key: `"<sheetId>!<A1>"`, e.g. `(1, 3, 2)` → `"1!B3"`. */
|
|
18
|
+
export function cellKey(sheetId, row, column) {
|
|
19
|
+
return `${sheetId}!${columnLabel(column)}${row}`;
|
|
20
|
+
}
|
|
21
|
+
/** 1-based column number for an A1 column label (`"A"` → 1, `"AA"` → 27). Inverse of
|
|
22
|
+
* {@link columnLabel}. */
|
|
23
|
+
export function columnNumberFromLabel(label) {
|
|
24
|
+
let n = 0;
|
|
25
|
+
for (let i = 0; i < label.length; i++) {
|
|
26
|
+
n = n * 26 + (label.toUpperCase().charCodeAt(i) - 64);
|
|
27
|
+
}
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
/** Inverse of {@link cellKey}: `"1!B3"` → `{ sheetId: 1, rowIndex: 3, columnIndex: 2 }`. Returns
|
|
31
|
+
* `null` for anything that isn't a well-formed key, so callers can skip it rather than guess. */
|
|
32
|
+
export function parseCellKey(key) {
|
|
33
|
+
const match = /^(\d+)!([A-Z]+)(\d+)$/.exec(key);
|
|
34
|
+
if (!match)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
sheetId: Number(match[1]),
|
|
38
|
+
columnIndex: columnNumberFromLabel(match[2]),
|
|
39
|
+
rowIndex: Number(match[3]),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GridRange } from "@rowsncolumns/common-types";
|
|
2
|
+
import type { Command } from "@rowsncolumns/spreadsheet-state";
|
|
3
|
+
import type { DocumentFacets } from "./index";
|
|
4
|
+
/**
|
|
5
|
+
* Conditional-format and data-validation rules are range-scoped, and the
|
|
6
|
+
* engine's `move-range` relocates only cells (and tables) — it never
|
|
7
|
+
* re-scopes rules. Without a follow-up, a cell whose fill/dropdown comes from
|
|
8
|
+
* a rule loses it the moment it is dragged outside the rule's range, while
|
|
9
|
+
* its manual `userEnteredFormat` travels fine.
|
|
10
|
+
*
|
|
11
|
+
* Given the post-move facets, compute the `update-conditional-format` /
|
|
12
|
+
* `update-data-validation` follow-up commands that carry the touched rules to
|
|
13
|
+
* the destination (Excel/Sheets behavior — see {@link rescopeSheetRangesOnMove}
|
|
14
|
+
* for the exact contract). Untouched rules produce no command, so a move that
|
|
15
|
+
* grazes no rule emits no engine re-evaluation and no extra undo entry.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rescopeRulesAfterMove(facets: Partial<Pick<DocumentFacets, "conditionalFormats" | "dataValidations">>, sheetId: number, from: GridRange, to: GridRange): Command[];
|
|
18
|
+
//# sourceMappingURL=move-rescope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"move-rescope.d.ts","sourceRoot":"","sources":["../../src/move-rescope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAG/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,oBAAoB,GAAG,iBAAiB,CAAC,CAAC,EAC/E,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,SAAS,GACZ,OAAO,EAAE,CAiCX"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { rescopeSheetRangesOnMove } from "@rowsncolumns/utils";
|
|
2
|
+
/**
|
|
3
|
+
* Conditional-format and data-validation rules are range-scoped, and the
|
|
4
|
+
* engine's `move-range` relocates only cells (and tables) — it never
|
|
5
|
+
* re-scopes rules. Without a follow-up, a cell whose fill/dropdown comes from
|
|
6
|
+
* a rule loses it the moment it is dragged outside the rule's range, while
|
|
7
|
+
* its manual `userEnteredFormat` travels fine.
|
|
8
|
+
*
|
|
9
|
+
* Given the post-move facets, compute the `update-conditional-format` /
|
|
10
|
+
* `update-data-validation` follow-up commands that carry the touched rules to
|
|
11
|
+
* the destination (Excel/Sheets behavior — see {@link rescopeSheetRangesOnMove}
|
|
12
|
+
* for the exact contract). Untouched rules produce no command, so a move that
|
|
13
|
+
* grazes no rule emits no engine re-evaluation and no extra undo entry.
|
|
14
|
+
*/
|
|
15
|
+
export function rescopeRulesAfterMove(facets, sheetId, from, to) {
|
|
16
|
+
const commands = [];
|
|
17
|
+
for (const rule of facets.conditionalFormats ?? []) {
|
|
18
|
+
const ranges = rescopeSheetRangesOnMove(rule.ranges ?? [], sheetId, from, to);
|
|
19
|
+
if (ranges) {
|
|
20
|
+
commands.push({
|
|
21
|
+
command: "update-conditional-format",
|
|
22
|
+
ruleId: rule.id,
|
|
23
|
+
rule: { ranges },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const rule of facets.dataValidations ?? []) {
|
|
28
|
+
const ranges = rescopeSheetRangesOnMove(rule.ranges ?? [], sheetId, from, to);
|
|
29
|
+
if (ranges) {
|
|
30
|
+
commands.push({
|
|
31
|
+
command: "update-data-validation",
|
|
32
|
+
ruleId: rule.id,
|
|
33
|
+
rule: { ranges },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return commands;
|
|
38
|
+
}
|