@rowsncolumns/toolkit 24.5.18 → 24.5.19
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/dist/cjs/excel-parser/defined-names.d.ts +18 -0
- package/dist/cjs/excel-parser/defined-names.d.ts.map +1 -0
- package/dist/cjs/excel-parser/defined-names.js +59 -0
- package/dist/cjs/excel-parser/interface.d.ts +7 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/function-table.d.ts +17 -0
- package/dist/cjs/excel-parser/plugins/xlsb/function-table.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsb/function-table.js +988 -0
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +45 -3
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +406 -374
- package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts +43 -9
- package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.js +422 -478
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +147 -191
- package/dist/cjs/excel-parser/types.d.ts +17 -0
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +1 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +4 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +41 -20
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/excel-parser/defined-names.d.ts +18 -0
- package/dist/esm/excel-parser/defined-names.d.ts.map +1 -0
- package/dist/esm/excel-parser/defined-names.js +67 -0
- package/dist/esm/excel-parser/interface.d.ts +7 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/function-table.d.ts +17 -0
- package/dist/esm/excel-parser/plugins/xlsb/function-table.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsb/function-table.js +988 -0
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +45 -3
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/index.js +266 -205
- package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts +43 -9
- package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.js +405 -472
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +9 -58
- package/dist/esm/excel-parser/types.d.ts +17 -0
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +1 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +3 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +22 -3
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,9 @@ import { getCellXfsKey, ssfFormat, uuid } from "@rowsncolumns/utils";
|
|
|
2
2
|
import { IndexedColors } from "@rowsncolumns/utils";
|
|
3
3
|
import { XMLParser } from "fast-xml-parser";
|
|
4
4
|
import { get } from "lodash-es";
|
|
5
|
-
import {
|
|
5
|
+
import { namedRangesForDefinedName } from "../../defined-names";
|
|
6
|
+
import { isBuiltinPrintName } from "../../../print-settings";
|
|
7
|
+
import { decodeRgce, parseRgce, quoteSheetName, RgceUnsupportedError, } from "./rgce-parser";
|
|
6
8
|
import { StreamingZipParser } from "../../zip-parser-streaming";
|
|
7
9
|
import { createFilterSpecsFromAutoFilter, createSortSpecsFromSortState, excelColumnWidthToPixels, getRangeFromAddressRef, hiddenFilterButtonColumns, parseTableColumnTotals, } from "../../helpers";
|
|
8
10
|
import { defaultSpreadsheetTheme } from "../../theme-default";
|
|
@@ -57,6 +59,7 @@ const RT = {
|
|
|
57
59
|
BrtSupBookSrc: 0x0163,
|
|
58
60
|
BrtSupSelf: 0x0165,
|
|
59
61
|
BrtSupSame: 0x0166,
|
|
62
|
+
BrtSupAddin: 0x029b,
|
|
60
63
|
BrtExternSheet: 0x016a,
|
|
61
64
|
BrtDrawing: 0x0226,
|
|
62
65
|
BrtBeginAFilter: 0x00a1,
|
|
@@ -74,7 +77,10 @@ const RT = {
|
|
|
74
77
|
BrtEndDVal: 0x0104,
|
|
75
78
|
BrtWbProp: 0x0099,
|
|
76
79
|
BrtCalcProp: 0x009d,
|
|
77
|
-
|
|
80
|
+
// [MS-XLSB] 2.3.2: BrtArrFmla 426, BrtShrFmla 427. A shared formula's
|
|
81
|
+
// cells each hold a PtgExp naming its first row.
|
|
82
|
+
BrtArrFmla: 0x01aa,
|
|
83
|
+
BrtShrFmla: 0x01ab,
|
|
78
84
|
BrtCFRule: 0x01df,
|
|
79
85
|
BrtDVal: 0x0103,
|
|
80
86
|
};
|
|
@@ -238,38 +244,31 @@ const parseBrtExternSheet = (payload) => {
|
|
|
238
244
|
}
|
|
239
245
|
return [];
|
|
240
246
|
};
|
|
247
|
+
const emptyFormulaDecodeStats = () => ({
|
|
248
|
+
decoded: 0,
|
|
249
|
+
unsupported: 0,
|
|
250
|
+
reasons: {},
|
|
251
|
+
definedNames: { decoded: 0, unsupported: 0 },
|
|
252
|
+
});
|
|
253
|
+
const PTG_EXP = 0x01;
|
|
254
|
+
/** A PtgExp: the token and the shared formula's first row (4 bytes). */
|
|
255
|
+
const PTG_EXP_LENGTH = 5;
|
|
256
|
+
const readUInt32 = (bytes, at) => new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(at, true);
|
|
241
257
|
const parseBrtShrFmla = (payload) => {
|
|
258
|
+
// RfX (rwFirst, rwLast, colFirst, colLast: 4 bytes each), then the formula:
|
|
259
|
+
// cce (4 bytes) and cce bytes of tokens ([MS-XLSB] 2.4.758).
|
|
242
260
|
if (payload.length < 20)
|
|
243
261
|
return null;
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
const startColZero = reader.readUInt32LE();
|
|
248
|
-
const endColZero = reader.readUInt32LE();
|
|
249
|
-
// cce can be preceded by small metadata in some files; try a few offsets.
|
|
250
|
-
const candidateOffsets = [16, 18, 20, 24];
|
|
251
|
-
let rgceBytes;
|
|
252
|
-
for (const offset of candidateOffsets) {
|
|
253
|
-
if (offset + 4 > payload.length)
|
|
254
|
-
continue;
|
|
255
|
-
const cce = new DataView(payload.buffer, payload.byteOffset + offset, 4).getUint32(0, true);
|
|
256
|
-
if (cce <= 0)
|
|
257
|
-
continue;
|
|
258
|
-
const start = offset + 4;
|
|
259
|
-
const end = start + cce;
|
|
260
|
-
if (end <= payload.length) {
|
|
261
|
-
rgceBytes = payload.subarray(start, end);
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (!rgceBytes || rgceBytes.length === 0)
|
|
262
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
263
|
+
const cce = view.getUint32(16, true);
|
|
264
|
+
if (cce === 0 || 20 + cce > payload.length)
|
|
266
265
|
return null;
|
|
267
266
|
return {
|
|
268
|
-
startRow:
|
|
269
|
-
endRow:
|
|
270
|
-
startCol:
|
|
271
|
-
endCol:
|
|
272
|
-
rgceBytes,
|
|
267
|
+
startRow: view.getUint32(0, true) + 1,
|
|
268
|
+
endRow: view.getUint32(4, true) + 1,
|
|
269
|
+
startCol: view.getUint32(8, true) + 1,
|
|
270
|
+
endCol: view.getUint32(12, true) + 1,
|
|
271
|
+
rgceBytes: payload.subarray(20, 20 + cce),
|
|
273
272
|
};
|
|
274
273
|
};
|
|
275
274
|
const parseWsView = (payload) => {
|
|
@@ -300,39 +299,46 @@ const parsePane = (payload) => {
|
|
|
300
299
|
frozenRowCount: isFrozen && ySplit > 0 ? Math.round(ySplit) : 0,
|
|
301
300
|
};
|
|
302
301
|
};
|
|
303
|
-
export const
|
|
302
|
+
export const readBrtName = (payload) => {
|
|
304
303
|
try {
|
|
305
304
|
const reader = new Reader(payload);
|
|
306
305
|
const flags = reader.readUInt32LE();
|
|
307
|
-
const fHidden = (flags & 0x00000001) !== 0;
|
|
308
306
|
reader.readUInt8(); // chKey
|
|
309
|
-
const itab = reader.readUInt32LE(); //
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return null; // Not enough data
|
|
313
|
-
}
|
|
307
|
+
const itab = reader.readUInt32LE(); // 0xFFFFFFFF: workbook scope
|
|
308
|
+
if (reader.offset + 4 > payload.length)
|
|
309
|
+
return null;
|
|
314
310
|
const name = reader.readXLWideString();
|
|
315
|
-
// NameParsedFormula: cce (
|
|
316
|
-
let
|
|
311
|
+
// NameParsedFormula: cce (4 bytes) and cce bytes of tokens.
|
|
312
|
+
let rgce;
|
|
317
313
|
if (reader.offset + 4 <= payload.length) {
|
|
318
314
|
const cce = reader.readUInt32LE();
|
|
319
315
|
if (cce > 0 && reader.offset + cce <= payload.length) {
|
|
320
|
-
|
|
321
|
-
formula = parseRgce(rgceBytes, resolveSheetNameByXti);
|
|
316
|
+
rgce = payload.subarray(reader.offset, reader.offset + cce);
|
|
322
317
|
}
|
|
323
318
|
}
|
|
324
319
|
return {
|
|
325
320
|
name,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
321
|
+
hidden: (flags & 0x00000001) !== 0,
|
|
322
|
+
builtin: (flags & 0x00000020) !== 0,
|
|
323
|
+
...(itab === 0xffffffff ? {} : { itab }),
|
|
324
|
+
...(rgce ? { rgce } : {}),
|
|
329
325
|
};
|
|
330
326
|
}
|
|
331
|
-
catch
|
|
332
|
-
// If parsing fails, return null to skip this named range
|
|
327
|
+
catch {
|
|
333
328
|
return null;
|
|
334
329
|
}
|
|
335
330
|
};
|
|
331
|
+
export const parseBrtNamePayload = (payload, resolveSheetNameByXti) => {
|
|
332
|
+
const record = readBrtName(payload);
|
|
333
|
+
if (!record)
|
|
334
|
+
return null;
|
|
335
|
+
return {
|
|
336
|
+
name: record.name,
|
|
337
|
+
formula: record.rgce ? parseRgce(record.rgce, resolveSheetNameByXti) : "",
|
|
338
|
+
hidden: record.hidden,
|
|
339
|
+
sheetIndex: record.itab,
|
|
340
|
+
};
|
|
341
|
+
};
|
|
336
342
|
/**
|
|
337
343
|
* Parse the `BrtWbProp` workbook-properties record (MS-XLSB 2.4.312).
|
|
338
344
|
* The payload starts with a 32-bit flags field; `fDate1904` is bit 0 of the
|
|
@@ -506,7 +512,12 @@ const getNumberFormatType = (pattern) => {
|
|
|
506
512
|
return "NUMBER";
|
|
507
513
|
return "GENERAL";
|
|
508
514
|
};
|
|
509
|
-
const parseCellFromRecord = (type, payload, row, prevCol,
|
|
515
|
+
const parseCellFromRecord = (type, payload, row, prevCol,
|
|
516
|
+
/**
|
|
517
|
+
* The formula text for a cell's tokens, or `undefined` to keep only its
|
|
518
|
+
* cached value. Omitted: formulas are not read.
|
|
519
|
+
*/
|
|
520
|
+
decodeFormula) => {
|
|
510
521
|
if (row <= 0)
|
|
511
522
|
return null;
|
|
512
523
|
const reader = new Reader(payload);
|
|
@@ -548,68 +559,26 @@ const parseCellFromRecord = (type, payload, row, prevCol, resolveSheetNameByXti,
|
|
|
548
559
|
value: "",
|
|
549
560
|
};
|
|
550
561
|
}
|
|
562
|
+
// CellParsedFormula ([MS-XLSB] 2.5.97.1): cce (4 bytes) and the tokens,
|
|
563
|
+
// then cb (4 bytes) and the tokens' extra data (a PtgExp's column).
|
|
551
564
|
const readFormula = () => {
|
|
552
|
-
if (!
|
|
565
|
+
if (!decodeFormula)
|
|
553
566
|
return undefined;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (start + 4 > payload.length)
|
|
567
|
-
return undefined;
|
|
568
|
-
const cce = new DataView(payload.buffer, payload.byteOffset + start, 4).getUint32(0, true);
|
|
569
|
-
if (cce <= 0)
|
|
570
|
-
return undefined;
|
|
571
|
-
const rgceStart = start + 4;
|
|
572
|
-
const rgceEnd = rgceStart + cce;
|
|
573
|
-
if (rgceEnd > payload.length)
|
|
574
|
-
return undefined;
|
|
575
|
-
return payload.subarray(rgceStart, rgceEnd);
|
|
576
|
-
}
|
|
577
|
-
if (start + 2 > payload.length)
|
|
578
|
-
return undefined;
|
|
579
|
-
const cce = new DataView(payload.buffer, payload.byteOffset + start, 2).getUint16(0, true);
|
|
580
|
-
if (cce <= 0)
|
|
581
|
-
return undefined;
|
|
582
|
-
const rgceStart = start + 2;
|
|
583
|
-
const rgceEnd = rgceStart + cce;
|
|
584
|
-
if (rgceEnd > payload.length)
|
|
585
|
-
return undefined;
|
|
586
|
-
return payload.subarray(rgceStart, rgceEnd);
|
|
587
|
-
};
|
|
588
|
-
const tryResolve = (parsed) => {
|
|
589
|
-
const expMatch = parsed.match(/^__PTGEXP__:(\d+):(\d+)$/);
|
|
590
|
-
if (!expMatch)
|
|
591
|
-
return parsed;
|
|
592
|
-
if (!resolveSharedFormula)
|
|
593
|
-
return undefined;
|
|
594
|
-
const sourceRow = Number(expMatch[1]);
|
|
595
|
-
const sourceCol = Number(expMatch[2]);
|
|
596
|
-
return resolveSharedFormula(sourceRow, sourceCol, row, colZero + 1);
|
|
597
|
-
};
|
|
598
|
-
for (const start of candidateStarts) {
|
|
599
|
-
for (const cceSize of [4, 2]) {
|
|
600
|
-
const rgceBytes = decodeAt(start, cceSize);
|
|
601
|
-
if (!rgceBytes || rgceBytes.length === 0)
|
|
602
|
-
continue;
|
|
603
|
-
const parsed = parseRgce(rgceBytes, resolveSheetNameByXti, { row, col: colZero + 1 }, true);
|
|
604
|
-
if (!parsed)
|
|
605
|
-
continue;
|
|
606
|
-
const resolved = tryResolve(parsed);
|
|
607
|
-
if (resolved && resolved.length > 0) {
|
|
608
|
-
return resolved;
|
|
609
|
-
}
|
|
567
|
+
if (reader.offset + 4 > payload.length)
|
|
568
|
+
return undefined;
|
|
569
|
+
const cce = reader.readUInt32LE();
|
|
570
|
+
if (cce === 0 || reader.offset + cce > payload.length)
|
|
571
|
+
return undefined;
|
|
572
|
+
const rgce = payload.subarray(reader.offset, reader.offset + cce);
|
|
573
|
+
reader.offset += cce;
|
|
574
|
+
let rgcb = new Uint8Array(0);
|
|
575
|
+
if (reader.offset + 4 <= payload.length) {
|
|
576
|
+
const cb = reader.readUInt32LE();
|
|
577
|
+
if (reader.offset + cb <= payload.length) {
|
|
578
|
+
rgcb = payload.subarray(reader.offset, reader.offset + cb);
|
|
610
579
|
}
|
|
611
580
|
}
|
|
612
|
-
return
|
|
581
|
+
return decodeFormula(rgce, { row, col: colZero + 1 }, rgcb);
|
|
613
582
|
};
|
|
614
583
|
if (type === RT.BrtFmlaNum) {
|
|
615
584
|
const value = reader.readFloat64LE();
|
|
@@ -639,8 +608,7 @@ const parseCellFromRecord = (type, payload, row, prevCol, resolveSheetNameByXti,
|
|
|
639
608
|
}
|
|
640
609
|
if (type === RT.BrtFmlaBool) {
|
|
641
610
|
const value = reader.readUInt8() !== 0;
|
|
642
|
-
reader.
|
|
643
|
-
reader.readUInt16LE(); // flags
|
|
611
|
+
reader.readUInt16LE(); // grbitFlags
|
|
644
612
|
const formula = readFormula();
|
|
645
613
|
return {
|
|
646
614
|
row,
|
|
@@ -653,8 +621,7 @@ const parseCellFromRecord = (type, payload, row, prevCol, resolveSheetNameByXti,
|
|
|
653
621
|
}
|
|
654
622
|
if (type === RT.BrtFmlaError) {
|
|
655
623
|
const value = reader.readUInt8();
|
|
656
|
-
reader.
|
|
657
|
-
reader.readUInt16LE(); // flags
|
|
624
|
+
reader.readUInt16LE(); // grbitFlags
|
|
658
625
|
const formula = readFormula();
|
|
659
626
|
return {
|
|
660
627
|
row,
|
|
@@ -1189,6 +1156,15 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1189
1156
|
calcProp = {};
|
|
1190
1157
|
supportingLinks = [];
|
|
1191
1158
|
externSheetRefs = [];
|
|
1159
|
+
/** Every sheet in workbook order, very hidden ones included; `sheetId` only for imported ones. */
|
|
1160
|
+
workbookSheets = [];
|
|
1161
|
+
/** Every `BrtName` in record order (`null`: unreadable): a `PtgName` is a 1-based index into it. */
|
|
1162
|
+
definedNames = [];
|
|
1163
|
+
/** A name's expression by its 1-based index; `null` when it does not decode. */
|
|
1164
|
+
definedNameExpressions = new Map();
|
|
1165
|
+
/** `${itab}:${lower-cased name}` of every sheet-local name. */
|
|
1166
|
+
sheetLocalNames = new Set();
|
|
1167
|
+
formulaDecodeStats = emptyFormulaDecodeStats();
|
|
1192
1168
|
use1904 = false;
|
|
1193
1169
|
async load(buffer, password) {
|
|
1194
1170
|
await this.zipParser.load(buffer, password);
|
|
@@ -1204,7 +1180,8 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1204
1180
|
const records = readAllRecordHeaders(workbookBinary);
|
|
1205
1181
|
const workbookRels = await this.loadWorkbookRelationships();
|
|
1206
1182
|
const sheets = [];
|
|
1207
|
-
const
|
|
1183
|
+
const workbookSheets = [];
|
|
1184
|
+
const definedNames = [];
|
|
1208
1185
|
const supportingLinks = [];
|
|
1209
1186
|
let externSheetRefs = [];
|
|
1210
1187
|
for (const record of records) {
|
|
@@ -1213,15 +1190,19 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1213
1190
|
const parsed = parseBundleSheet(payload);
|
|
1214
1191
|
// Skip veryHidden sheets (hidden === 2) - these are internal sheets not meant for users
|
|
1215
1192
|
if (parsed.hidden === 2) {
|
|
1193
|
+
workbookSheets.push({ title: parsed.title });
|
|
1216
1194
|
continue;
|
|
1217
1195
|
}
|
|
1218
1196
|
const relTarget = workbookRels.get(parsed.rid);
|
|
1219
1197
|
const path = relTarget
|
|
1220
1198
|
? resolveRelativePath("xl/workbook.bin", relTarget)
|
|
1221
1199
|
: undefined;
|
|
1200
|
+
const sheetId = parsed.tabId || sheets.length + 1;
|
|
1201
|
+
const title = parsed.title || `Sheet${sheets.length + 1}`;
|
|
1222
1202
|
sheets.push({
|
|
1223
|
-
sheetId
|
|
1224
|
-
title
|
|
1203
|
+
sheetId,
|
|
1204
|
+
title,
|
|
1205
|
+
workbookIndex: workbookSheets.length,
|
|
1225
1206
|
hidden: parsed.hidden === 1,
|
|
1226
1207
|
rid: parsed.rid,
|
|
1227
1208
|
path,
|
|
@@ -1232,6 +1213,7 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1232
1213
|
rowMetadata: [],
|
|
1233
1214
|
columnMetadata: [],
|
|
1234
1215
|
});
|
|
1216
|
+
workbookSheets.push({ title, sheetId });
|
|
1235
1217
|
}
|
|
1236
1218
|
else if (record.type === RT.BrtSupSelf) {
|
|
1237
1219
|
supportingLinks.push("self");
|
|
@@ -1242,6 +1224,10 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1242
1224
|
else if (record.type === RT.BrtSupBookSrc) {
|
|
1243
1225
|
supportingLinks.push("external");
|
|
1244
1226
|
}
|
|
1227
|
+
else if (record.type === RT.BrtSupAddin) {
|
|
1228
|
+
// Counted so the XTIs after it index the right supporting link.
|
|
1229
|
+
supportingLinks.push("addin");
|
|
1230
|
+
}
|
|
1245
1231
|
else if (record.type === RT.BrtExternSheet) {
|
|
1246
1232
|
const parsed = parseBrtExternSheet(payload);
|
|
1247
1233
|
if (parsed.length > 0) {
|
|
@@ -1249,10 +1235,9 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1249
1235
|
}
|
|
1250
1236
|
}
|
|
1251
1237
|
else if (record.type === RT.BrtName) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1238
|
+
// Decoded once every sheet and name is known: a name may refer to
|
|
1239
|
+
// sheets and names that come after it.
|
|
1240
|
+
definedNames.push(readBrtName(payload));
|
|
1256
1241
|
}
|
|
1257
1242
|
else if (record.type === RT.BrtCalcProp) {
|
|
1258
1243
|
this.parseBrtCalcProp(payload);
|
|
@@ -1264,10 +1249,17 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1264
1249
|
}
|
|
1265
1250
|
}
|
|
1266
1251
|
}
|
|
1267
|
-
this.namedRanges = namedRanges;
|
|
1268
1252
|
this.supportingLinks = supportingLinks;
|
|
1269
1253
|
this.externSheetRefs = externSheetRefs;
|
|
1254
|
+
this.workbookSheets = workbookSheets;
|
|
1255
|
+
this.definedNames = definedNames;
|
|
1256
|
+
this.definedNameExpressions.clear();
|
|
1257
|
+
this.sheetLocalNames = new Set(definedNames.flatMap((record) => record?.itab === undefined
|
|
1258
|
+
? []
|
|
1259
|
+
: [`${record.itab}:${record.name.toLowerCase()}`]));
|
|
1260
|
+
this.formulaDecodeStats = emptyFormulaDecodeStats();
|
|
1270
1261
|
this.sheets = sheets;
|
|
1262
|
+
this.namedRanges = this.buildNamedRanges();
|
|
1271
1263
|
this.sharedStrings = await this.loadSharedStrings();
|
|
1272
1264
|
this.styles = await this.loadStyles();
|
|
1273
1265
|
this.theme = await this.loadTheme();
|
|
@@ -1682,7 +1674,7 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1682
1674
|
}
|
|
1683
1675
|
if (!isCellRecord(record.type))
|
|
1684
1676
|
continue;
|
|
1685
|
-
const parsed = parseCellFromRecord(record.type, payload, currentRow, prevCol
|
|
1677
|
+
const parsed = parseCellFromRecord(record.type, payload, currentRow, prevCol);
|
|
1686
1678
|
if (!parsed)
|
|
1687
1679
|
continue;
|
|
1688
1680
|
prevCol = parsed.col - 1;
|
|
@@ -1711,25 +1703,128 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1711
1703
|
getSheetNames() {
|
|
1712
1704
|
return this.sheets.map((s) => s.title);
|
|
1713
1705
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1706
|
+
/**
|
|
1707
|
+
* The imported sheet an XTI names, or `undefined` when a formula cannot
|
|
1708
|
+
* name it exactly: another workbook, a span of sheets, a deleted or very
|
|
1709
|
+
* hidden sheet.
|
|
1710
|
+
*/
|
|
1711
|
+
sheetNameByXti(ixti) {
|
|
1717
1712
|
const xti = this.externSheetRefs[ixti];
|
|
1718
1713
|
if (!xti)
|
|
1719
|
-
return
|
|
1714
|
+
return undefined;
|
|
1720
1715
|
const linkType = this.supportingLinks[xti.iSupBook];
|
|
1721
|
-
if (linkType !== "self" && linkType !== "same")
|
|
1722
|
-
return
|
|
1716
|
+
if (linkType !== "self" && linkType !== "same")
|
|
1717
|
+
return undefined;
|
|
1718
|
+
if (xti.itabFirst !== xti.itabLast)
|
|
1719
|
+
return undefined;
|
|
1720
|
+
const sheet = this.workbookSheets[xti.itabFirst];
|
|
1721
|
+
return sheet?.sheetId === undefined ? undefined : sheet.title;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* A defined name's expression (OOXML's spelling, no leading `=`), or
|
|
1725
|
+
* `undefined` when it does not decode exactly — then no formula may refer
|
|
1726
|
+
* to it either. `visiting` breaks names that refer to themselves.
|
|
1727
|
+
*/
|
|
1728
|
+
definedNameExpression(index, visiting = new Set()) {
|
|
1729
|
+
const known = this.definedNameExpressions.get(index);
|
|
1730
|
+
if (known !== undefined)
|
|
1731
|
+
return known ?? undefined;
|
|
1732
|
+
const record = this.definedNames[index - 1];
|
|
1733
|
+
if (!record?.rgce || visiting.has(index))
|
|
1734
|
+
return undefined;
|
|
1735
|
+
if (record.itab !== undefined &&
|
|
1736
|
+
this.workbookSheets[record.itab]?.sheetId === undefined) {
|
|
1737
|
+
this.definedNameExpressions.set(index, null);
|
|
1738
|
+
return undefined;
|
|
1739
|
+
}
|
|
1740
|
+
visiting.add(index);
|
|
1741
|
+
let expression = null;
|
|
1742
|
+
try {
|
|
1743
|
+
expression = decodeRgce(record.rgce, {
|
|
1744
|
+
sheetName: (ixti) => this.sheetNameByXti(ixti),
|
|
1745
|
+
definedName: (target) => this.definedNameIn(target, record.itab, visiting),
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
catch (error) {
|
|
1749
|
+
if (!(error instanceof RgceUnsupportedError))
|
|
1750
|
+
throw error;
|
|
1723
1751
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1752
|
+
finally {
|
|
1753
|
+
visiting.delete(index);
|
|
1754
|
+
}
|
|
1755
|
+
this.definedNameExpressions.set(index, expression);
|
|
1756
|
+
return expression ?? undefined;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* How a formula on the sheet at `formulaItab` (a name's own formula: the
|
|
1760
|
+
* sheet it is local to) writes the name a `PtgName` points at: bare on its
|
|
1761
|
+
* own sheet or for a workbook name, `Sheet!Name` elsewhere. `undefined`
|
|
1762
|
+
* when the name does not survive the import, or when a local name of the
|
|
1763
|
+
* same spelling would capture a bare workbook name.
|
|
1764
|
+
*/
|
|
1765
|
+
definedNameIn(index, formulaItab, visiting) {
|
|
1766
|
+
const record = this.definedNames[index - 1];
|
|
1767
|
+
if (!record || record.builtin)
|
|
1768
|
+
return undefined;
|
|
1769
|
+
if (this.definedNameExpression(index, visiting) === undefined) {
|
|
1770
|
+
return undefined;
|
|
1771
|
+
}
|
|
1772
|
+
if (record.itab === undefined) {
|
|
1773
|
+
const shadowed = formulaItab !== undefined &&
|
|
1774
|
+
this.sheetLocalNames.has(`${formulaItab}:${record.name.toLowerCase()}`);
|
|
1775
|
+
return shadowed ? undefined : record.name;
|
|
1776
|
+
}
|
|
1777
|
+
if (record.itab === formulaItab)
|
|
1778
|
+
return record.name;
|
|
1779
|
+
const sheet = this.workbookSheets[record.itab];
|
|
1780
|
+
return sheet?.sheetId === undefined
|
|
1781
|
+
? undefined
|
|
1782
|
+
: `${quoteSheetName(sheet.title)}!${record.name}`;
|
|
1783
|
+
}
|
|
1784
|
+
/** Every name that decodes, the way the XLSX plugin reads `<definedName>`. */
|
|
1785
|
+
buildNamedRanges() {
|
|
1786
|
+
const sheetIdBySheetName = {};
|
|
1787
|
+
for (const sheet of this.sheets) {
|
|
1788
|
+
sheetIdBySheetName[sheet.title] = sheet.sheetId;
|
|
1789
|
+
}
|
|
1790
|
+
const namedRanges = [];
|
|
1791
|
+
const counts = this.formulaDecodeStats.definedNames;
|
|
1792
|
+
this.definedNames.forEach((record, at) => {
|
|
1793
|
+
// Print areas and titles are sheet print facets, not user names.
|
|
1794
|
+
const expression = record && !isBuiltinPrintName(record.name)
|
|
1795
|
+
? this.definedNameExpression(at + 1)
|
|
1796
|
+
: undefined;
|
|
1797
|
+
if (!record || expression === undefined) {
|
|
1798
|
+
counts.unsupported += 1;
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
counts.decoded += 1;
|
|
1802
|
+
namedRanges.push(...namedRangesForDefinedName({
|
|
1803
|
+
name: record.name,
|
|
1804
|
+
expression,
|
|
1805
|
+
hidden: record.hidden,
|
|
1806
|
+
scopeSheetId: record.itab === undefined
|
|
1807
|
+
? undefined
|
|
1808
|
+
: this.workbookSheets[record.itab]?.sheetId,
|
|
1809
|
+
sheetIdBySheetName,
|
|
1810
|
+
}));
|
|
1811
|
+
});
|
|
1812
|
+
return namedRanges;
|
|
1813
|
+
}
|
|
1814
|
+
getFormulaDecodeStats() {
|
|
1815
|
+
return {
|
|
1816
|
+
...this.formulaDecodeStats,
|
|
1817
|
+
reasons: { ...this.formulaDecodeStats.reasons },
|
|
1818
|
+
definedNames: { ...this.formulaDecodeStats.definedNames },
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
countUnsupportedFormula(reason) {
|
|
1822
|
+
const stats = this.formulaDecodeStats;
|
|
1823
|
+
stats.unsupported += 1;
|
|
1824
|
+
const key = reason in stats.reasons || Object.keys(stats.reasons).length < 32
|
|
1825
|
+
? reason
|
|
1826
|
+
: "other";
|
|
1827
|
+
stats.reasons[key] = (stats.reasons[key] ?? 0) + 1;
|
|
1733
1828
|
}
|
|
1734
1829
|
async getSheets(minRowCount = 0, minColumnCount = 0) {
|
|
1735
1830
|
const tables = [];
|
|
@@ -1807,28 +1902,55 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1807
1902
|
continue;
|
|
1808
1903
|
const binary = await this.zipParser.getBinary(sheet.path);
|
|
1809
1904
|
const records = readAllRecordHeaders(binary);
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
// appears later in the record stream.
|
|
1905
|
+
// Shared formulas by their first cell: each cell of one holds a PtgExp
|
|
1906
|
+
// naming that cell, and its BrtShrFmla may come after the first cell.
|
|
1907
|
+
const sharedFormulas = new Map();
|
|
1814
1908
|
for (const record of records) {
|
|
1815
1909
|
if (record.type !== RT.BrtShrFmla)
|
|
1816
1910
|
continue;
|
|
1817
|
-
const
|
|
1818
|
-
const parsedShared = parseBrtShrFmla(payload);
|
|
1911
|
+
const parsedShared = parseBrtShrFmla(binary.subarray(record.dataOffset, record.nextOffset));
|
|
1819
1912
|
if (!parsedShared)
|
|
1820
1913
|
continue;
|
|
1821
|
-
|
|
1822
|
-
sharedFormulaRanges.push(parsedShared);
|
|
1914
|
+
sharedFormulas.set(`${parsedShared.startRow}:${parsedShared.startCol}`, parsedShared);
|
|
1823
1915
|
}
|
|
1916
|
+
const decodeFormula = (rgce, cell, rgcb) => {
|
|
1917
|
+
let tokens = rgce;
|
|
1918
|
+
if (rgce.length === PTG_EXP_LENGTH && rgce[0] === PTG_EXP) {
|
|
1919
|
+
// PtgExp: the shared formula's first row, its column in the extra data.
|
|
1920
|
+
const shared = rgcb.length >= 4
|
|
1921
|
+
? sharedFormulas.get(`${readUInt32(rgce, 1) + 1}:${readUInt32(rgcb, 0) + 1}`)
|
|
1922
|
+
: undefined;
|
|
1923
|
+
if (!shared ||
|
|
1924
|
+
cell.row < shared.startRow ||
|
|
1925
|
+
cell.row > shared.endRow ||
|
|
1926
|
+
cell.col < shared.startCol ||
|
|
1927
|
+
cell.col > shared.endCol) {
|
|
1928
|
+
this.countUnsupportedFormula("array formula");
|
|
1929
|
+
return undefined;
|
|
1930
|
+
}
|
|
1931
|
+
tokens = shared.rgceBytes;
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
const formula = decodeRgce(tokens, {
|
|
1935
|
+
sheetName: (ixti) => this.sheetNameByXti(ixti),
|
|
1936
|
+
definedName: (index) => this.definedNameIn(index, sheet.workbookIndex),
|
|
1937
|
+
baseCell: cell,
|
|
1938
|
+
});
|
|
1939
|
+
this.formulaDecodeStats.decoded += 1;
|
|
1940
|
+
return formula;
|
|
1941
|
+
}
|
|
1942
|
+
catch (error) {
|
|
1943
|
+
if (!(error instanceof RgceUnsupportedError))
|
|
1944
|
+
throw error;
|
|
1945
|
+
this.countUnsupportedFormula(error.message);
|
|
1946
|
+
return undefined;
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1824
1949
|
let currentRow = 0;
|
|
1825
1950
|
let prevCol = -1;
|
|
1826
1951
|
const rows = new Map();
|
|
1827
1952
|
for (const record of records) {
|
|
1828
1953
|
const payload = binary.subarray(record.dataOffset, record.nextOffset);
|
|
1829
|
-
if (record.type === RT.BrtShrFmla) {
|
|
1830
|
-
continue;
|
|
1831
|
-
}
|
|
1832
1954
|
if (record.type === RT.BrtRowHdr) {
|
|
1833
1955
|
const parsedRow = parseRowHeader(payload);
|
|
1834
1956
|
currentRow = parsedRow.row;
|
|
@@ -1840,36 +1962,9 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
1840
1962
|
}
|
|
1841
1963
|
if (!isCellRecord(record.type))
|
|
1842
1964
|
continue;
|
|
1843
|
-
const parsed = parseCellFromRecord(record.type, payload, currentRow, prevCol,
|
|
1844
|
-
let rgce = sharedFormulaBytes.get(`${sourceRow}:${sourceCol}`);
|
|
1845
|
-
if (!rgce) {
|
|
1846
|
-
const sourceRange = sharedFormulaRanges.find((range) => sourceRow >= range.startRow &&
|
|
1847
|
-
sourceRow <= range.endRow &&
|
|
1848
|
-
sourceCol >= range.startCol &&
|
|
1849
|
-
sourceCol <= range.endCol);
|
|
1850
|
-
rgce = sourceRange?.rgceBytes;
|
|
1851
|
-
}
|
|
1852
|
-
if (!rgce)
|
|
1853
|
-
return undefined;
|
|
1854
|
-
return parseRgce(rgce, (ixti) => this.resolveSheetNameByXti(ixti), {
|
|
1855
|
-
row: targetRow,
|
|
1856
|
-
col: targetCol,
|
|
1857
|
-
}, true); // Enable debug logging
|
|
1858
|
-
});
|
|
1965
|
+
const parsed = parseCellFromRecord(record.type, payload, currentRow, prevCol, decodeFormula);
|
|
1859
1966
|
if (!parsed)
|
|
1860
1967
|
continue;
|
|
1861
|
-
if (!parsed.formula && sharedFormulaRanges.length > 0) {
|
|
1862
|
-
const sharedRange = sharedFormulaRanges.find((range) => parsed.row >= range.startRow &&
|
|
1863
|
-
parsed.row <= range.endRow &&
|
|
1864
|
-
parsed.col >= range.startCol &&
|
|
1865
|
-
parsed.col <= range.endCol);
|
|
1866
|
-
if (sharedRange) {
|
|
1867
|
-
const resolved = parseRgce(sharedRange.rgceBytes, (ixti) => this.resolveSheetNameByXti(ixti), { row: parsed.row, col: parsed.col }, true);
|
|
1868
|
-
if (resolved) {
|
|
1869
|
-
parsed.formula = resolved;
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
1968
|
prevCol = parsed.col - 1;
|
|
1874
1969
|
let row = rows.get(parsed.row);
|
|
1875
1970
|
if (!row) {
|
|
@@ -2295,40 +2390,6 @@ export class XlsbPlugin extends BaseWorkbookPlugin {
|
|
|
2295
2390
|
}
|
|
2296
2391
|
return validations;
|
|
2297
2392
|
}
|
|
2298
|
-
parseBrtName(payload, sheets) {
|
|
2299
|
-
const parsed = parseBrtNamePayload(payload, (ixti) => this.resolveSheetNameByXti(ixti));
|
|
2300
|
-
// Skip if parsing failed or hidden names
|
|
2301
|
-
if (!parsed || parsed.hidden) {
|
|
2302
|
-
return undefined;
|
|
2303
|
-
}
|
|
2304
|
-
// Try to parse the formula as a range reference
|
|
2305
|
-
const gridRange = getRangeFromAddressRef(parsed.formula);
|
|
2306
|
-
// Determine the sheet ID if this is a sheet-scoped name
|
|
2307
|
-
let sheetId;
|
|
2308
|
-
if (parsed.sheetIndex !== undefined && parsed.sheetIndex < sheets.length) {
|
|
2309
|
-
sheetId = sheets[parsed.sheetIndex].sheetId;
|
|
2310
|
-
}
|
|
2311
|
-
// Create the NamedRange object
|
|
2312
|
-
const namedRange = {
|
|
2313
|
-
namedRangeId: uuid(),
|
|
2314
|
-
name: parsed.name,
|
|
2315
|
-
};
|
|
2316
|
-
// Set either range or value depending on what we parsed
|
|
2317
|
-
if (gridRange) {
|
|
2318
|
-
// Convert GridRange to SheetRange by adding sheetId
|
|
2319
|
-
// Use the sheet-scoped sheetId if available, otherwise use the first sheet
|
|
2320
|
-
const rangeSheetId = sheetId ?? (sheets.length > 0 ? sheets[0].sheetId : 1);
|
|
2321
|
-
namedRange.range = {
|
|
2322
|
-
...gridRange,
|
|
2323
|
-
sheetId: rangeSheetId,
|
|
2324
|
-
};
|
|
2325
|
-
}
|
|
2326
|
-
else if (parsed.formula) {
|
|
2327
|
-
// If we can't parse it as a range, store it as a formula value
|
|
2328
|
-
namedRange.value = parsed.formula;
|
|
2329
|
-
}
|
|
2330
|
-
return namedRange;
|
|
2331
|
-
}
|
|
2332
2393
|
parseBrtCalcProp(payload) {
|
|
2333
2394
|
const parsed = parseBrtCalcPropPayload(payload);
|
|
2334
2395
|
if (!parsed) {
|