@rowsncolumns/toolkit 24.5.17 → 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.
Files changed (53) hide show
  1. package/dist/cjs/excel-parser/defined-names.d.ts +18 -0
  2. package/dist/cjs/excel-parser/defined-names.d.ts.map +1 -0
  3. package/dist/cjs/excel-parser/defined-names.js +59 -0
  4. package/dist/cjs/excel-parser/interface.d.ts +7 -1
  5. package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
  6. package/dist/cjs/excel-parser/plugins/xlsb/function-table.d.ts +17 -0
  7. package/dist/cjs/excel-parser/plugins/xlsb/function-table.d.ts.map +1 -0
  8. package/dist/cjs/excel-parser/plugins/xlsb/function-table.js +988 -0
  9. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +45 -3
  10. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  11. package/dist/cjs/excel-parser/plugins/xlsb/index.js +406 -374
  12. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts +43 -9
  13. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -1
  14. package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.js +422 -478
  15. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  16. package/dist/cjs/excel-parser/plugins/xlsx/index.js +147 -191
  17. package/dist/cjs/excel-parser/types.d.ts +17 -0
  18. package/dist/cjs/excel-parser/types.d.ts.map +1 -1
  19. package/dist/cjs/excel-parser/workbook.d.ts +1 -0
  20. package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
  21. package/dist/cjs/excel-parser/workbook.js +4 -0
  22. package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
  23. package/dist/cjs/exporter/ods/ods-export.js +6 -3
  24. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  25. package/dist/cjs/exporter/xlsx/xlsx-export.js +47 -23
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  27. package/dist/esm/excel-parser/defined-names.d.ts +18 -0
  28. package/dist/esm/excel-parser/defined-names.d.ts.map +1 -0
  29. package/dist/esm/excel-parser/defined-names.js +67 -0
  30. package/dist/esm/excel-parser/interface.d.ts +7 -1
  31. package/dist/esm/excel-parser/interface.d.ts.map +1 -1
  32. package/dist/esm/excel-parser/plugins/xlsb/function-table.d.ts +17 -0
  33. package/dist/esm/excel-parser/plugins/xlsb/function-table.d.ts.map +1 -0
  34. package/dist/esm/excel-parser/plugins/xlsb/function-table.js +988 -0
  35. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +45 -3
  36. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  37. package/dist/esm/excel-parser/plugins/xlsb/index.js +266 -205
  38. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts +43 -9
  39. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -1
  40. package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.js +405 -472
  41. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  42. package/dist/esm/excel-parser/plugins/xlsx/index.js +9 -58
  43. package/dist/esm/excel-parser/types.d.ts +17 -0
  44. package/dist/esm/excel-parser/types.d.ts.map +1 -1
  45. package/dist/esm/excel-parser/workbook.d.ts +1 -0
  46. package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
  47. package/dist/esm/excel-parser/workbook.js +3 -0
  48. package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
  49. package/dist/esm/exporter/ods/ods-export.js +6 -3
  50. package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  51. package/dist/esm/exporter/xlsx/xlsx-export.js +28 -6
  52. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  53. package/package.json +2 -2
@@ -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 { parseRgce } from "./rgce-parser";
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
- BrtShrFmla: 0x0018,
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 reader = new Reader(payload);
245
- const startRowZero = reader.readUInt32LE();
246
- const endRowZero = reader.readUInt32LE();
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: startRowZero + 1,
269
- endRow: endRowZero + 1,
270
- startCol: startColZero + 1,
271
- endCol: endColZero + 1,
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 parseBrtNamePayload = (payload, resolveSheetNameByXti) => {
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(); // sheet index (0xFFFFFFFF for workbook scope)
310
- // Read the name string
311
- if (reader.offset + 4 > payload.length) {
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 (UInt32) followed by cce bytes of Rgce
316
- let formula = "";
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
- const rgceBytes = payload.subarray(reader.offset, reader.offset + cce);
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
- formula,
327
- hidden: fHidden,
328
- sheetIndex: itab === 0xffffffff ? undefined : itab,
321
+ hidden: (flags & 0x00000001) !== 0,
322
+ builtin: (flags & 0x00000020) !== 0,
323
+ ...(itab === 0xffffffff ? {} : { itab }),
324
+ ...(rgce ? { rgce } : {}),
329
325
  };
330
326
  }
331
- catch (err) {
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, resolveSheetNameByXti, resolveSharedFormula, parseFormula = true) => {
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 (!parseFormula)
565
+ if (!decodeFormula)
553
566
  return undefined;
554
- const candidateStarts = [
555
- reader.offset - 2,
556
- reader.offset,
557
- reader.offset + 2,
558
- reader.offset + 4,
559
- reader.offset + 6,
560
- reader.offset + 8,
561
- reader.offset + 10,
562
- reader.offset + 12,
563
- ].filter((start, idx, arr) => start >= 0 && arr.indexOf(start) === idx);
564
- const decodeAt = (start, cceSize) => {
565
- if (cceSize === 4) {
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 undefined;
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.readUInt8(); // padding
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.readUInt8(); // padding
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 namedRanges = [];
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: parsed.tabId || sheets.length + 1,
1224
- title: parsed.title || `Sheet${sheets.length + 1}`,
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
- const namedRange = this.parseBrtName(payload, sheets);
1253
- if (namedRange) {
1254
- namedRanges.push(namedRange);
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, (ixti) => this.resolveSheetNameByXti(ixti), undefined, false);
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
- resolveSheetNameByXti(ixti) {
1715
- const sheetNames = this.getSheetNames();
1716
- const fallback = sheetNames[ixti] ?? sheetNames[ixti - 1];
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 fallback;
1714
+ return undefined;
1720
1715
  const linkType = this.supportingLinks[xti.iSupBook];
1721
- if (linkType !== "self" && linkType !== "same") {
1722
- return fallback;
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
- const first = xti.itabFirst;
1725
- const last = xti.itabLast;
1726
- if (first < 0 || first >= sheetNames.length)
1727
- return fallback;
1728
- if (last < 0 || last >= sheetNames.length)
1729
- return sheetNames[first];
1730
- if (first === last)
1731
- return sheetNames[first];
1732
- return `${sheetNames[first]}:${sheetNames[last]}`;
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
- const sharedFormulaBytes = new Map();
1811
- const sharedFormulaRanges = [];
1812
- // Pre-scan shared formulas so PtgExp can resolve even when BrtShrFmla
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 payload = binary.subarray(record.dataOffset, record.nextOffset);
1818
- const parsedShared = parseBrtShrFmla(payload);
1911
+ const parsedShared = parseBrtShrFmla(binary.subarray(record.dataOffset, record.nextOffset));
1819
1912
  if (!parsedShared)
1820
1913
  continue;
1821
- sharedFormulaBytes.set(`${parsedShared.startRow}:${parsedShared.startCol}`, parsedShared.rgceBytes);
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, (ixti) => this.resolveSheetNameByXti(ixti), (sourceRow, sourceCol, targetRow, targetCol) => {
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) {