@niicojs/excel 0.3.4 → 0.3.6

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/index.cjs CHANGED
@@ -91,6 +91,45 @@ var fflate = require('fflate');
91
91
  }
92
92
  return `${start}:${end}`;
93
93
  };
94
+ /**
95
+ * Parses a qualified sheet + address reference.
96
+ * Supports Sheet!A1 and 'Sheet Name'!A1.
97
+ */ const parseSheetAddress = (reference)=>{
98
+ const exclamationIndex = reference.lastIndexOf('!');
99
+ if (exclamationIndex <= 0 || exclamationIndex >= reference.length - 1) {
100
+ throw new Error(`Invalid sheet address reference: ${reference}`);
101
+ }
102
+ const rawSheet = reference.slice(0, exclamationIndex);
103
+ const addressPart = reference.slice(exclamationIndex + 1);
104
+ const sheet = unquoteSheetName(rawSheet);
105
+ return {
106
+ sheet,
107
+ address: parseAddress(addressPart)
108
+ };
109
+ };
110
+ /**
111
+ * Parses a qualified sheet + range reference.
112
+ * Supports Sheet!A1:B10 and 'Sheet Name'!A1:B10.
113
+ */ const parseSheetRange = (reference)=>{
114
+ const exclamationIndex = reference.lastIndexOf('!');
115
+ if (exclamationIndex <= 0 || exclamationIndex >= reference.length - 1) {
116
+ throw new Error(`Invalid sheet range reference: ${reference}`);
117
+ }
118
+ const rawSheet = reference.slice(0, exclamationIndex);
119
+ const rangePart = reference.slice(exclamationIndex + 1);
120
+ const sheet = unquoteSheetName(rawSheet);
121
+ return {
122
+ sheet,
123
+ range: parseRange(rangePart)
124
+ };
125
+ };
126
+ const unquoteSheetName = (sheet)=>{
127
+ const trimmed = sheet.trim();
128
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
129
+ return trimmed.slice(1, -1).replace(/''/g, "'");
130
+ }
131
+ return trimmed;
132
+ };
94
133
  /**
95
134
  * Normalizes a range so start is always top-left and end is bottom-right
96
135
  */ const normalizeRange = (range)=>{
@@ -314,6 +353,10 @@ const formatDatePart = (value, token, locale)=>{
314
353
  return padNumber(value.getMonth() + 1, 2);
315
354
  case 'm':
316
355
  return String(value.getMonth() + 1);
356
+ case 'dddd':
357
+ return value.toLocaleString(locale, {
358
+ weekday: 'long'
359
+ });
317
360
  case 'dd':
318
361
  return padNumber(value.getDate(), 2);
319
362
  case 'd':
@@ -373,6 +416,7 @@ const tokenizeDateFormat = (format)=>{
373
416
  'mmm',
374
417
  'mm',
375
418
  'm',
419
+ 'dddd',
376
420
  'dd',
377
421
  'd',
378
422
  'hh',
@@ -412,6 +456,9 @@ const isDateFormat = (format)=>{
412
456
  return /[ymdhss]/.test(lowered);
413
457
  };
414
458
  const formatDate = (value, format, locale)=>{
459
+ if (locale === 'fr-FR' && format === '[$-F800]dddd\\,\\ mmmm\\ dd\\,\\ yyyy') {
460
+ format = 'dddd, dd mmmm yyyy';
461
+ }
415
462
  const tokens = tokenizeDateFormat(format);
416
463
  return tokens.map((token)=>formatDatePart(value, token, locale)).join('');
417
464
  };
@@ -879,7 +926,10 @@ const parserOptions = {
879
926
  cdataPropName: '#cdata',
880
927
  trimValues: false,
881
928
  parseTagValue: false,
882
- parseAttributeValue: false
929
+ parseAttributeValue: false,
930
+ processEntities: {
931
+ maxTotalExpansions: 10_000
932
+ }
883
933
  };
884
934
  // Builder options matching parser for round-trip compatibility
885
935
  const builderOptions = {
@@ -890,7 +940,7 @@ const builderOptions = {
890
940
  commentPropName: '#comment',
891
941
  cdataPropName: '#cdata',
892
942
  format: false,
893
- suppressEmptyNode: false,
943
+ suppressEmptyNode: true,
894
944
  suppressBooleanAttributes: false
895
945
  };
896
946
  const parser = new fastXmlParser.XMLParser(parserOptions);
@@ -1276,15 +1326,14 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1276
1326
  }
1277
1327
  }
1278
1328
 
1279
- /**
1280
- * Represents a worksheet in a workbook
1329
+ /**
1330
+ * Represents a worksheet in a workbook
1281
1331
  */ class Worksheet {
1282
1332
  constructor(workbook, name){
1283
1333
  this._cells = new Map();
1284
1334
  this._xmlNodes = null;
1285
1335
  this._dirty = false;
1286
1336
  this._mergedCells = new Set();
1287
- this._sheetData = [];
1288
1337
  this._columnWidths = new Map();
1289
1338
  this._rowHeights = new Map();
1290
1339
  this._frozenPane = null;
@@ -1292,36 +1341,57 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1292
1341
  this._boundsDirty = true;
1293
1342
  this._tables = [];
1294
1343
  this._preserveXml = false;
1344
+ this._rawXml = null;
1345
+ this._lazyParse = false;
1295
1346
  this._tableRelIds = null;
1347
+ this._pivotTableRelIds = null;
1296
1348
  this._sheetViewsDirty = false;
1297
1349
  this._colsDirty = false;
1298
1350
  this._tablePartsDirty = false;
1351
+ this._pivotTablePartsDirty = false;
1299
1352
  this._workbook = workbook;
1300
1353
  this._name = name;
1301
1354
  }
1302
- /**
1303
- * Get the workbook this sheet belongs to
1355
+ /**
1356
+ * Get the workbook this sheet belongs to
1304
1357
  */ get workbook() {
1305
1358
  return this._workbook;
1306
1359
  }
1307
- /**
1308
- * Get the sheet name
1360
+ /**
1361
+ * Get the sheet name
1309
1362
  */ get name() {
1310
1363
  return this._name;
1311
1364
  }
1312
- /**
1313
- * Set the sheet name
1365
+ /**
1366
+ * Set the sheet name
1314
1367
  */ set name(value) {
1315
1368
  this._name = value;
1316
1369
  this._dirty = true;
1317
1370
  }
1318
- /**
1319
- * Parse worksheet XML content
1320
- */ parse(xml) {
1321
- this._xmlNodes = parseXml(xml);
1371
+ /**
1372
+ * Parse worksheet XML content
1373
+ */ parse(xml, options = {}) {
1374
+ this._rawXml = xml;
1375
+ this._xmlNodes = null;
1376
+ this._preserveXml = true;
1377
+ this._lazyParse = options.lazy ?? true;
1378
+ if (!this._lazyParse) {
1379
+ this._ensureParsed();
1380
+ }
1381
+ }
1382
+ _ensureParsed() {
1383
+ if (!this._lazyParse) return;
1384
+ if (!this._rawXml) {
1385
+ this._lazyParse = false;
1386
+ return;
1387
+ }
1388
+ this._xmlNodes = parseXml(this._rawXml);
1322
1389
  this._preserveXml = true;
1323
1390
  const worksheet = findElement(this._xmlNodes, 'worksheet');
1324
- if (!worksheet) return;
1391
+ if (!worksheet) {
1392
+ this._lazyParse = false;
1393
+ return;
1394
+ }
1325
1395
  const worksheetChildren = getChildren(worksheet, 'worksheet');
1326
1396
  // Parse sheet views (freeze panes)
1327
1397
  const sheetViews = findElement(worksheetChildren, 'sheetViews');
@@ -1346,8 +1416,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1346
1416
  // Parse sheet data (cells)
1347
1417
  const sheetData = findElement(worksheetChildren, 'sheetData');
1348
1418
  if (sheetData) {
1349
- this._sheetData = getChildren(sheetData, 'sheetData');
1350
- this._parseSheetData(this._sheetData);
1419
+ const rows = getChildren(sheetData, 'sheetData');
1420
+ this._parseSheetData(rows);
1351
1421
  }
1352
1422
  // Parse column widths
1353
1423
  const cols = findElement(worksheetChildren, 'cols');
@@ -1379,9 +1449,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1379
1449
  }
1380
1450
  }
1381
1451
  }
1452
+ this._lazyParse = false;
1382
1453
  }
1383
- /**
1384
- * Parse the sheetData element to extract cells
1454
+ /**
1455
+ * Parse the sheetData element to extract cells
1385
1456
  */ _parseSheetData(rows) {
1386
1457
  for (const rowNode of rows){
1387
1458
  if (!('row' in rowNode)) continue;
@@ -1403,8 +1474,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1403
1474
  }
1404
1475
  this._boundsDirty = true;
1405
1476
  }
1406
- /**
1407
- * Parse a cell XML node to CellData
1477
+ /**
1478
+ * Parse a cell XML node to CellData
1408
1479
  */ _parseCellNode(node) {
1409
1480
  const data = {};
1410
1481
  // Type attribute
@@ -1479,9 +1550,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1479
1550
  }
1480
1551
  return data;
1481
1552
  }
1482
- /**
1483
- * Get a cell by address or row/col
1553
+ /**
1554
+ * Get a cell by address or row/col
1484
1555
  */ cell(rowOrAddress, col) {
1556
+ this._ensureParsed();
1485
1557
  const { row, col: c } = parseCellRef(rowOrAddress, col);
1486
1558
  const address = toAddress(row, c);
1487
1559
  let cell = this._cells.get(address);
@@ -1492,14 +1564,16 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1492
1564
  }
1493
1565
  return cell;
1494
1566
  }
1495
- /**
1496
- * Get an existing cell without creating it.
1567
+ /**
1568
+ * Get an existing cell without creating it.
1497
1569
  */ getCellIfExists(rowOrAddress, col) {
1570
+ this._ensureParsed();
1498
1571
  const { row, col: c } = parseCellRef(rowOrAddress, col);
1499
1572
  const address = toAddress(row, c);
1500
1573
  return this._cells.get(address);
1501
1574
  }
1502
1575
  range(startRowOrRange, startCol, endRow, endCol) {
1576
+ this._ensureParsed();
1503
1577
  let rangeAddr;
1504
1578
  if (typeof startRowOrRange === 'string') {
1505
1579
  rangeAddr = parseRange(startRowOrRange);
@@ -1520,9 +1594,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1520
1594
  }
1521
1595
  return new Range(this, rangeAddr);
1522
1596
  }
1523
- /**
1524
- * Merge cells in the given range
1597
+ /**
1598
+ * Merge cells in the given range
1525
1599
  */ mergeCells(rangeOrStart, end) {
1600
+ this._ensureParsed();
1526
1601
  let rangeStr;
1527
1602
  if (end) {
1528
1603
  rangeStr = `${rangeOrStart}:${end}`;
@@ -1532,34 +1607,39 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1532
1607
  this._mergedCells.add(rangeStr);
1533
1608
  this._dirty = true;
1534
1609
  }
1535
- /**
1536
- * Unmerge cells in the given range
1610
+ /**
1611
+ * Unmerge cells in the given range
1537
1612
  */ unmergeCells(rangeStr) {
1613
+ this._ensureParsed();
1538
1614
  this._mergedCells.delete(rangeStr);
1539
1615
  this._dirty = true;
1540
1616
  }
1541
- /**
1542
- * Get all merged cell ranges
1617
+ /**
1618
+ * Get all merged cell ranges
1543
1619
  */ get mergedCells() {
1620
+ this._ensureParsed();
1544
1621
  return Array.from(this._mergedCells);
1545
1622
  }
1546
- /**
1547
- * Check if the worksheet has been modified
1623
+ /**
1624
+ * Check if the worksheet has been modified
1548
1625
  */ get dirty() {
1626
+ this._ensureParsed();
1549
1627
  if (this._dirty) return true;
1550
1628
  for (const cell of this._cells.values()){
1551
1629
  if (cell.dirty) return true;
1552
1630
  }
1553
1631
  return false;
1554
1632
  }
1555
- /**
1556
- * Get all cells in the worksheet
1633
+ /**
1634
+ * Get all cells in the worksheet
1557
1635
  */ get cells() {
1636
+ this._ensureParsed();
1558
1637
  return this._cells;
1559
1638
  }
1560
- /**
1561
- * Set a column width (0-based index or column letter)
1639
+ /**
1640
+ * Set a column width (0-based index or column letter)
1562
1641
  */ setColumnWidth(col, width) {
1642
+ this._ensureParsed();
1563
1643
  if (!Number.isFinite(width) || width <= 0) {
1564
1644
  throw new Error('Column width must be a positive number');
1565
1645
  }
@@ -1571,15 +1651,17 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1571
1651
  this._colsDirty = true;
1572
1652
  this._dirty = true;
1573
1653
  }
1574
- /**
1575
- * Get a column width if set
1654
+ /**
1655
+ * Get a column width if set
1576
1656
  */ getColumnWidth(col) {
1657
+ this._ensureParsed();
1577
1658
  const colIndex = typeof col === 'number' ? col : letterToCol(col);
1578
1659
  return this._columnWidths.get(colIndex);
1579
1660
  }
1580
- /**
1581
- * Set a row height (0-based index)
1661
+ /**
1662
+ * Set a row height (0-based index)
1582
1663
  */ setRowHeight(row, height) {
1664
+ this._ensureParsed();
1583
1665
  if (!Number.isFinite(height) || height <= 0) {
1584
1666
  throw new Error('Row height must be a positive number');
1585
1667
  }
@@ -1590,14 +1672,16 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1590
1672
  this._colsDirty = true;
1591
1673
  this._dirty = true;
1592
1674
  }
1593
- /**
1594
- * Get a row height if set
1675
+ /**
1676
+ * Get a row height if set
1595
1677
  */ getRowHeight(row) {
1678
+ this._ensureParsed();
1596
1679
  return this._rowHeights.get(row);
1597
1680
  }
1598
- /**
1599
- * Freeze panes at a given row/column split (counts from top-left)
1681
+ /**
1682
+ * Freeze panes at a given row/column split (counts from top-left)
1600
1683
  */ freezePane(rowSplit, colSplit) {
1684
+ this._ensureParsed();
1601
1685
  if (rowSplit < 0 || colSplit < 0) {
1602
1686
  throw new Error('Freeze pane splits must be >= 0');
1603
1687
  }
@@ -1612,69 +1696,85 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1612
1696
  this._sheetViewsDirty = true;
1613
1697
  this._dirty = true;
1614
1698
  }
1615
- /**
1616
- * Get current frozen pane configuration
1699
+ /**
1700
+ * Get current frozen pane configuration
1617
1701
  */ getFrozenPane() {
1702
+ this._ensureParsed();
1618
1703
  return this._frozenPane ? {
1619
1704
  ...this._frozenPane
1620
1705
  } : null;
1621
1706
  }
1622
- /**
1623
- * Get all tables in the worksheet
1707
+ /**
1708
+ * Get all tables in the worksheet
1624
1709
  */ get tables() {
1710
+ this._ensureParsed();
1625
1711
  return [
1626
1712
  ...this._tables
1627
1713
  ];
1628
1714
  }
1629
- /**
1630
- * Get column width entries
1631
- * @internal
1715
+ /**
1716
+ * Get column width entries
1717
+ * @internal
1632
1718
  */ getColumnWidths() {
1719
+ this._ensureParsed();
1633
1720
  return new Map(this._columnWidths);
1634
1721
  }
1635
- /**
1636
- * Get row height entries
1637
- * @internal
1722
+ /**
1723
+ * Get row height entries
1724
+ * @internal
1638
1725
  */ getRowHeights() {
1726
+ this._ensureParsed();
1639
1727
  return new Map(this._rowHeights);
1640
1728
  }
1641
- /**
1642
- * Set table relationship IDs for tableParts generation.
1643
- * @internal
1729
+ /**
1730
+ * Set table relationship IDs for tableParts generation.
1731
+ * @internal
1644
1732
  */ setTableRelIds(ids) {
1733
+ this._ensureParsed();
1645
1734
  this._tableRelIds = ids ? [
1646
1735
  ...ids
1647
1736
  ] : null;
1648
1737
  this._tablePartsDirty = true;
1649
1738
  }
1650
- /**
1651
- * Create an Excel Table (ListObject) from a data range.
1652
- *
1653
- * Tables provide structured data features like auto-filter, banded styling,
1654
- * and total row with aggregation functions.
1655
- *
1656
- * @param config - Table configuration
1657
- * @returns Table instance for method chaining
1658
- *
1659
- * @example
1660
- * ```typescript
1661
- * // Create a table with default styling
1662
- * const table = sheet.createTable({
1663
- * name: 'SalesData',
1664
- * range: 'A1:D10',
1665
- * });
1666
- *
1667
- * // Create a table with total row
1668
- * const table = sheet.createTable({
1669
- * name: 'SalesData',
1670
- * range: 'A1:D10',
1671
- * totalRow: true,
1672
- * style: { name: 'TableStyleMedium2' }
1673
- * });
1674
- *
1675
- * table.setTotalFunction('Sales', 'sum');
1676
- * ```
1739
+ /**
1740
+ * Set pivot table relationship IDs for pivotTableParts generation.
1741
+ * @internal
1742
+ */ setPivotTableRelIds(ids) {
1743
+ this._ensureParsed();
1744
+ this._pivotTableRelIds = ids ? [
1745
+ ...ids
1746
+ ] : null;
1747
+ this._pivotTablePartsDirty = true;
1748
+ }
1749
+ /**
1750
+ * Create an Excel Table (ListObject) from a data range.
1751
+ *
1752
+ * Tables provide structured data features like auto-filter, banded styling,
1753
+ * and total row with aggregation functions.
1754
+ *
1755
+ * @param config - Table configuration
1756
+ * @returns Table instance for method chaining
1757
+ *
1758
+ * @example
1759
+ * ```typescript
1760
+ * // Create a table with default styling
1761
+ * const table = sheet.createTable({
1762
+ * name: 'SalesData',
1763
+ * range: 'A1:D10',
1764
+ * });
1765
+ *
1766
+ * // Create a table with total row
1767
+ * const table = sheet.createTable({
1768
+ * name: 'SalesData',
1769
+ * range: 'A1:D10',
1770
+ * totalRow: true,
1771
+ * style: { name: 'TableStyleMedium2' }
1772
+ * });
1773
+ *
1774
+ * table.setTotalFunction('Sales', 'sum');
1775
+ * ```
1677
1776
  */ createTable(config) {
1777
+ this._ensureParsed();
1678
1778
  // Validate table name is unique within the workbook
1679
1779
  for (const sheet of this._workbook.sheetNames){
1680
1780
  const ws = this._workbook.sheet(sheet);
@@ -1696,24 +1796,25 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1696
1796
  this._dirty = true;
1697
1797
  return table;
1698
1798
  }
1699
- /**
1799
+ /**
1700
1800
  * Convert sheet data to an array of JSON objects.
1701
- *
1801
+ *
1702
1802
  * @param config - Configuration options
1703
1803
  * @returns Array of objects where keys are field names and values are cell values
1704
1804
  *
1705
1805
  * @example
1706
- * ```typescript
1707
- * // Using first row as headers
1708
- * const data = sheet.toJson();
1709
- *
1710
- * // Using custom field names
1711
- * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
1712
- *
1713
- * // Starting from a specific row/column
1714
- * const data = sheet.toJson({ startRow: 2, startCol: 1 });
1715
- * ```
1806
+ * ```typescript
1807
+ * // Using first row as headers
1808
+ * const data = sheet.toJson();
1809
+ *
1810
+ * // Using custom field names
1811
+ * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
1812
+ *
1813
+ * // Starting from a specific row/column
1814
+ * const data = sheet.toJson({ startRow: 2, startCol: 1 });
1815
+ * ```
1716
1816
  */ toJson(config = {}) {
1817
+ this._ensureParsed();
1717
1818
  const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling, asText = false, locale } = config;
1718
1819
  // Get the bounds of data in the sheet
1719
1820
  const bounds = this._getDataBounds();
@@ -1785,8 +1886,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1785
1886
  }
1786
1887
  return value;
1787
1888
  }
1788
- /**
1789
- * Get the bounds of data in the sheet (min/max row and column with data)
1889
+ /**
1890
+ * Get the bounds of data in the sheet (min/max row and column with data)
1790
1891
  */ _getDataBounds() {
1791
1892
  if (!this._boundsDirty && this._dataBoundsCache) {
1792
1893
  return this._dataBoundsCache;
@@ -1822,9 +1923,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1822
1923
  this._boundsDirty = false;
1823
1924
  return this._dataBoundsCache;
1824
1925
  }
1825
- /**
1826
- * Generate XML for this worksheet
1926
+ /**
1927
+ * Generate XML for this worksheet
1827
1928
  */ toXml() {
1929
+ if (this._lazyParse && !this._dirty && this._rawXml) {
1930
+ return this._rawXml;
1931
+ }
1932
+ this._ensureParsed();
1828
1933
  const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;
1829
1934
  // Build sheetData from cells
1830
1935
  const sheetDataNode = this._buildSheetDataNode();
@@ -1899,6 +2004,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
1899
2004
  if (tablePartsNode) {
1900
2005
  worksheetChildren.push(tablePartsNode);
1901
2006
  }
2007
+ const pivotTablePartsNode = this._buildPivotTablePartsNode();
2008
+ if (pivotTablePartsNode) {
2009
+ worksheetChildren.push(pivotTablePartsNode);
2010
+ }
1902
2011
  const worksheetNode = createElement('worksheet', {
1903
2012
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
1904
2013
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
@@ -2023,6 +2132,15 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2023
2132
  count: String(this._tables.length)
2024
2133
  }, tablePartNodes);
2025
2134
  }
2135
+ _buildPivotTablePartsNode() {
2136
+ if (!this._pivotTableRelIds || this._pivotTableRelIds.length === 0) return null;
2137
+ const pivotPartNodes = this._pivotTableRelIds.map((relId)=>createElement('pivotTablePart', {
2138
+ 'r:id': relId
2139
+ }, []));
2140
+ return createElement('pivotTableParts', {
2141
+ count: String(pivotPartNodes.length)
2142
+ }, pivotPartNodes);
2143
+ }
2026
2144
  _buildPreservedWorksheet() {
2027
2145
  if (!this._xmlNodes) return null;
2028
2146
  const worksheet = findElement(this._xmlNodes, 'worksheet');
@@ -2056,10 +2174,14 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2056
2174
  const tablePartsNode = this._buildTablePartsNode();
2057
2175
  upsertChild('tableParts', tablePartsNode);
2058
2176
  }
2177
+ if (this._pivotTablePartsDirty) {
2178
+ const pivotTablePartsNode = this._buildPivotTablePartsNode();
2179
+ upsertChild('pivotTableParts', pivotTablePartsNode);
2180
+ }
2059
2181
  return worksheet;
2060
2182
  }
2061
- /**
2062
- * Build a cell XML node from a Cell object
2183
+ /**
2184
+ * Build a cell XML node from a Cell object
2063
2185
  */ _buildCellNode(cell) {
2064
2186
  const data = cell.data;
2065
2187
  const attrs = {
@@ -2099,32 +2221,45 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2099
2221
  * Parse shared strings from XML content
2100
2222
  */ static parse(xml) {
2101
2223
  const ss = new SharedStrings();
2102
- const parsed = parseXml(xml);
2224
+ ss._rawXml = xml;
2225
+ ss._parse();
2226
+ return ss;
2227
+ }
2228
+ _parse() {
2229
+ if (this._parsed) return;
2230
+ if (!this._rawXml) {
2231
+ this._parsed = true;
2232
+ return;
2233
+ }
2234
+ const parsed = parseXml(this._rawXml);
2103
2235
  const sst = findElement(parsed, 'sst');
2104
- if (!sst) return ss;
2236
+ if (!sst) {
2237
+ this._parsed = true;
2238
+ return;
2239
+ }
2105
2240
  const countAttr = getAttr(sst, 'count');
2106
2241
  if (countAttr) {
2107
2242
  const total = parseInt(countAttr, 10);
2108
2243
  if (Number.isFinite(total) && total >= 0) {
2109
- ss._totalCount = total;
2244
+ this._totalCount = total;
2110
2245
  }
2111
2246
  }
2112
2247
  const children = getChildren(sst, 'sst');
2113
2248
  for (const child of children){
2114
2249
  if ('si' in child) {
2115
2250
  const siChildren = getChildren(child, 'si');
2116
- const text = ss.extractText(siChildren);
2117
- ss.entries.push({
2251
+ const text = this.extractText(siChildren);
2252
+ this.entries.push({
2118
2253
  text,
2119
2254
  node: child
2120
2255
  });
2121
- ss.stringToIndex.set(text, ss.entries.length - 1);
2256
+ this.stringToIndex.set(text, this.entries.length - 1);
2122
2257
  }
2123
2258
  }
2124
- if (ss._totalCount === 0 && ss.entries.length > 0) {
2125
- ss._totalCount = ss.entries.length;
2259
+ if (this._totalCount === 0 && this.entries.length > 0) {
2260
+ this._totalCount = this.entries.length;
2126
2261
  }
2127
- return ss;
2262
+ this._parsed = true;
2128
2263
  }
2129
2264
  /**
2130
2265
  * Extract text from a string item (si element)
@@ -2160,12 +2295,14 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2160
2295
  /**
2161
2296
  * Get a string by index
2162
2297
  */ getString(index) {
2298
+ this._parse();
2163
2299
  return this.entries[index]?.text;
2164
2300
  }
2165
2301
  /**
2166
2302
  * Add a string and return its index
2167
2303
  * If the string already exists, returns the existing index
2168
2304
  */ addString(str) {
2305
+ this._parse();
2169
2306
  const existing = this.stringToIndex.get(str);
2170
2307
  if (existing !== undefined) {
2171
2308
  this._totalCount++;
@@ -2193,26 +2330,31 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2193
2330
  /**
2194
2331
  * Check if the shared strings table has been modified
2195
2332
  */ get dirty() {
2333
+ this._parse();
2196
2334
  return this._dirty;
2197
2335
  }
2198
2336
  /**
2199
2337
  * Get the count of strings
2200
2338
  */ get count() {
2339
+ this._parse();
2201
2340
  return this.entries.length;
2202
2341
  }
2203
2342
  /**
2204
2343
  * Get total usage count of shared strings
2205
2344
  */ get totalCount() {
2345
+ this._parse();
2206
2346
  return Math.max(this._totalCount, this.entries.length);
2207
2347
  }
2208
2348
  /**
2209
2349
  * Get all unique shared strings in insertion order.
2210
2350
  */ getAllStrings() {
2351
+ this._parse();
2211
2352
  return this.entries.map((entry)=>entry.text);
2212
2353
  }
2213
2354
  /**
2214
2355
  * Generate XML for the shared strings table
2215
2356
  */ toXml() {
2357
+ this._parse();
2216
2358
  const siElements = [];
2217
2359
  for (const entry of this.entries){
2218
2360
  if (entry.node) {
@@ -2245,6 +2387,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2245
2387
  this.stringToIndex = new Map();
2246
2388
  this._dirty = false;
2247
2389
  this._totalCount = 0;
2390
+ this._rawXml = null;
2391
+ this._parsed = false;
2248
2392
  }
2249
2393
  }
2250
2394
 
@@ -2437,9 +2581,26 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2437
2581
  * Parse styles from XML content
2438
2582
  */ static parse(xml) {
2439
2583
  const styles = new Styles();
2440
- styles._xmlNodes = parseXml(xml);
2441
- const styleSheet = findElement(styles._xmlNodes, 'styleSheet');
2442
- if (!styleSheet) return styles;
2584
+ styles._rawXml = xml;
2585
+ styles._parse();
2586
+ return styles;
2587
+ }
2588
+ _parse() {
2589
+ if (this._parsed) return;
2590
+ if (!this._rawXml) {
2591
+ this._parsed = true;
2592
+ return;
2593
+ }
2594
+ this._xmlNodes = parseXml(this._rawXml);
2595
+ if (!this._xmlNodes) {
2596
+ this._parsed = true;
2597
+ return;
2598
+ }
2599
+ const styleSheet = findElement(this._xmlNodes, 'styleSheet');
2600
+ if (!styleSheet) {
2601
+ this._parsed = true;
2602
+ return;
2603
+ }
2443
2604
  const children = getChildren(styleSheet, 'styleSheet');
2444
2605
  // Parse number formats
2445
2606
  const numFmts = findElement(children, 'numFmts');
@@ -2448,7 +2609,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2448
2609
  if ('numFmt' in child) {
2449
2610
  const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);
2450
2611
  const code = getAttr(child, 'formatCode') || '';
2451
- styles._numFmts.set(id, code);
2612
+ this._numFmts.set(id, code);
2452
2613
  }
2453
2614
  }
2454
2615
  }
@@ -2457,7 +2618,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2457
2618
  if (fonts) {
2458
2619
  for (const child of getChildren(fonts, 'fonts')){
2459
2620
  if ('font' in child) {
2460
- styles._fonts.push(styles._parseFont(child));
2621
+ this._fonts.push(this._parseFont(child));
2461
2622
  }
2462
2623
  }
2463
2624
  }
@@ -2466,7 +2627,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2466
2627
  if (fills) {
2467
2628
  for (const child of getChildren(fills, 'fills')){
2468
2629
  if ('fill' in child) {
2469
- styles._fills.push(styles._parseFill(child));
2630
+ this._fills.push(this._parseFill(child));
2470
2631
  }
2471
2632
  }
2472
2633
  }
@@ -2475,7 +2636,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2475
2636
  if (borders) {
2476
2637
  for (const child of getChildren(borders, 'borders')){
2477
2638
  if ('border' in child) {
2478
- styles._borders.push(styles._parseBorder(child));
2639
+ this._borders.push(this._parseBorder(child));
2479
2640
  }
2480
2641
  }
2481
2642
  }
@@ -2484,16 +2645,17 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2484
2645
  if (cellXfs) {
2485
2646
  for (const child of getChildren(cellXfs, 'cellXfs')){
2486
2647
  if ('xf' in child) {
2487
- styles._cellXfs.push(styles._parseCellXf(child));
2648
+ this._cellXfs.push(this._parseCellXf(child));
2488
2649
  }
2489
2650
  }
2490
2651
  }
2491
- return styles;
2652
+ this._parsed = true;
2492
2653
  }
2493
2654
  /**
2494
2655
  * Create an empty styles object with defaults
2495
2656
  */ static createDefault() {
2496
2657
  const styles = new Styles();
2658
+ styles._parsed = true;
2497
2659
  // Default font (Calibri 11)
2498
2660
  styles._fonts.push({
2499
2661
  bold: false,
@@ -2633,6 +2795,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2633
2795
  /**
2634
2796
  * Get a style by index
2635
2797
  */ getStyle(index) {
2798
+ this._parse();
2636
2799
  const cached = this._styleObjectCache.get(index);
2637
2800
  if (cached) return {
2638
2801
  ...cached
@@ -2699,6 +2862,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2699
2862
  * Create a style and return its index
2700
2863
  * Uses caching to deduplicate identical styles
2701
2864
  */ createStyle(style) {
2865
+ this._parse();
2702
2866
  const key = this._getStyleKey(style);
2703
2867
  const cached = this._styleCache.get(key);
2704
2868
  if (cached !== undefined) {
@@ -2739,6 +2903,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2739
2903
  /**
2740
2904
  * Clone an existing style by index, optionally overriding fields.
2741
2905
  */ cloneStyle(index, overrides = {}) {
2906
+ this._parse();
2742
2907
  const baseStyle = this.getStyle(index);
2743
2908
  return this.createStyle({
2744
2909
  ...baseStyle,
@@ -2826,17 +2991,20 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
2826
2991
  * Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).
2827
2992
  * @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')
2828
2993
  */ getOrCreateNumFmtId(format) {
2994
+ this._parse();
2829
2995
  this._dirty = true;
2830
2996
  return this._findOrCreateNumFmt(format);
2831
2997
  }
2832
2998
  /**
2833
2999
  * Check if styles have been modified
2834
3000
  */ get dirty() {
3001
+ this._parse();
2835
3002
  return this._dirty;
2836
3003
  }
2837
3004
  /**
2838
3005
  * Generate XML for styles
2839
3006
  */ toXml() {
3007
+ this._parse();
2840
3008
  const children = [];
2841
3009
  // Number formats
2842
3010
  if (this._numFmts.size > 0) {
@@ -3028,6 +3196,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
3028
3196
  this._borders = [];
3029
3197
  this._cellXfs = []; // Cell formats (combined style index)
3030
3198
  this._xmlNodes = null;
3199
+ this._rawXml = null;
3200
+ this._parsed = false;
3031
3201
  this._dirty = false;
3032
3202
  // Cache for style deduplication
3033
3203
  this._styleCache = new Map();
@@ -3035,313 +3205,361 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
3035
3205
  }
3036
3206
  }
3037
3207
 
3208
+ const AGGREGATION_TO_XML = {
3209
+ sum: 'sum',
3210
+ count: 'count',
3211
+ average: 'average',
3212
+ min: 'min',
3213
+ max: 'max'
3214
+ };
3215
+ const SORT_TO_XML = {
3216
+ asc: 'ascending',
3217
+ desc: 'descending'
3218
+ };
3038
3219
  /**
3039
- * Represents an Excel pivot table with a fluent API for configuration.
3220
+ * Represents an Excel PivotTable with a fluent configuration API.
3040
3221
  */ class PivotTable {
3041
- constructor(name, cache, targetSheet, targetCell, targetRow, targetCol, pivotTableIndex, cacheFileIndex){
3222
+ constructor(workbook, config, sourceSheetName, sourceSheet, sourceRange, targetSheetName, targetCell, cacheId, pivotId, cachePartIndex, fields){
3042
3223
  this._rowFields = [];
3043
3224
  this._columnFields = [];
3044
- this._valueFields = [];
3045
3225
  this._filterFields = [];
3046
- this._fieldAssignments = new Map();
3047
- this._styles = null;
3048
- this._name = name;
3049
- this._cache = cache;
3050
- this._targetSheet = targetSheet;
3226
+ this._valueFields = [];
3227
+ this._sortOrders = new Map();
3228
+ this._filters = new Map();
3229
+ this._workbook = workbook;
3230
+ this._name = config.name;
3231
+ this._sourceSheetName = sourceSheetName;
3232
+ this._sourceSheet = sourceSheet;
3233
+ this._sourceRange = sourceRange;
3234
+ this._targetSheetName = targetSheetName;
3051
3235
  this._targetCell = targetCell;
3052
- this._targetRow = targetRow;
3053
- this._targetCol = targetCol;
3054
- this._pivotTableIndex = pivotTableIndex;
3055
- this._cacheFileIndex = cacheFileIndex;
3236
+ this._refreshOnLoad = config.refreshOnLoad !== false;
3237
+ this._cacheId = cacheId;
3238
+ this._pivotId = pivotId;
3239
+ this._cachePartIndex = cachePartIndex;
3240
+ this._fields = fields;
3056
3241
  }
3057
- /**
3058
- * Get the pivot table name
3059
- */ get name() {
3242
+ get name() {
3060
3243
  return this._name;
3061
3244
  }
3062
- /**
3063
- * Get the target sheet name
3064
- */ get targetSheet() {
3065
- return this._targetSheet;
3245
+ get sourceSheetName() {
3246
+ return this._sourceSheetName;
3066
3247
  }
3067
- /**
3068
- * Get the target cell address
3069
- */ get targetCell() {
3070
- return this._targetCell;
3248
+ get sourceRange() {
3249
+ return {
3250
+ start: {
3251
+ ...this._sourceRange.start
3252
+ },
3253
+ end: {
3254
+ ...this._sourceRange.end
3255
+ }
3256
+ };
3071
3257
  }
3072
- /**
3073
- * Get the pivot cache
3074
- */ get cache() {
3075
- return this._cache;
3258
+ get targetSheetName() {
3259
+ return this._targetSheetName;
3076
3260
  }
3077
- /**
3078
- * Get the pivot table index (for file naming)
3079
- */ get index() {
3080
- return this._pivotTableIndex;
3261
+ get targetCell() {
3262
+ return {
3263
+ ...this._targetCell
3264
+ };
3081
3265
  }
3082
- /**
3083
- * Get the pivot cache file index used for rels.
3084
- * @internal
3085
- */ get cacheFileIndex() {
3086
- return this._cacheFileIndex;
3266
+ get refreshOnLoad() {
3267
+ return this._refreshOnLoad;
3087
3268
  }
3088
- /**
3089
- * Set the styles reference for number format resolution
3090
- * @internal
3091
- */ setStyles(styles) {
3092
- this._styles = styles;
3269
+ get cacheId() {
3270
+ return this._cacheId;
3271
+ }
3272
+ get pivotId() {
3273
+ return this._pivotId;
3274
+ }
3275
+ get cachePartIndex() {
3276
+ return this._cachePartIndex;
3277
+ }
3278
+ addRowField(fieldName) {
3279
+ this._assertFieldExists(fieldName);
3280
+ if (!this._rowFields.includes(fieldName)) {
3281
+ this._rowFields.push(fieldName);
3282
+ }
3093
3283
  return this;
3094
3284
  }
3095
- /**
3096
- * Add a field to the row area
3097
- * @param fieldName - Name of the source field (column header)
3098
- */ addRowField(fieldName) {
3099
- const fieldIndex = this._cache.getFieldIndex(fieldName);
3100
- if (fieldIndex < 0) {
3101
- throw new Error(`Field not found in source data: ${fieldName}`);
3285
+ addColumnField(fieldName) {
3286
+ this._assertFieldExists(fieldName);
3287
+ if (!this._columnFields.includes(fieldName)) {
3288
+ this._columnFields.push(fieldName);
3102
3289
  }
3103
- const assignment = {
3104
- fieldName,
3105
- fieldIndex,
3106
- axis: 'row'
3107
- };
3108
- this._rowFields.push(assignment);
3109
- this._fieldAssignments.set(fieldIndex, assignment);
3110
3290
  return this;
3111
3291
  }
3112
- /**
3113
- * Add a field to the column area
3114
- * @param fieldName - Name of the source field (column header)
3115
- */ addColumnField(fieldName) {
3116
- const fieldIndex = this._cache.getFieldIndex(fieldName);
3117
- if (fieldIndex < 0) {
3118
- throw new Error(`Field not found in source data: ${fieldName}`);
3292
+ addFilterField(fieldName) {
3293
+ this._assertFieldExists(fieldName);
3294
+ if (!this._filterFields.includes(fieldName)) {
3295
+ this._filterFields.push(fieldName);
3119
3296
  }
3120
- const assignment = {
3121
- fieldName,
3122
- fieldIndex,
3123
- axis: 'column'
3124
- };
3125
- this._columnFields.push(assignment);
3126
- this._fieldAssignments.set(fieldIndex, assignment);
3127
3297
  return this;
3128
3298
  }
3129
3299
  addValueField(fieldNameOrConfig, aggregation = 'sum', displayName, numberFormat) {
3130
- // Normalize arguments to a common form
3131
- let fieldName;
3132
- let agg;
3133
- let name;
3134
- let format;
3135
- if (typeof fieldNameOrConfig === 'object') {
3136
- fieldName = fieldNameOrConfig.field;
3137
- agg = fieldNameOrConfig.aggregation ?? 'sum';
3138
- name = fieldNameOrConfig.name;
3139
- format = fieldNameOrConfig.numberFormat;
3300
+ let config;
3301
+ if (typeof fieldNameOrConfig === 'string') {
3302
+ config = {
3303
+ field: fieldNameOrConfig,
3304
+ aggregation,
3305
+ name: displayName,
3306
+ numberFormat
3307
+ };
3140
3308
  } else {
3141
- fieldName = fieldNameOrConfig;
3142
- agg = aggregation;
3143
- name = displayName;
3144
- format = numberFormat;
3145
- }
3146
- const fieldIndex = this._cache.getFieldIndex(fieldName);
3147
- if (fieldIndex < 0) {
3148
- throw new Error(`Field not found in source data: ${fieldName}`);
3149
- }
3150
- const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
3151
- // Resolve numFmtId immediately if format is provided and styles are available
3152
- let numFmtId;
3153
- if (format && this._styles) {
3154
- numFmtId = this._styles.getOrCreateNumFmtId(format);
3155
- }
3156
- const assignment = {
3157
- fieldName,
3158
- fieldIndex,
3159
- axis: 'value',
3160
- aggregation: agg,
3161
- displayName: name || defaultName,
3162
- numFmtId
3163
- };
3164
- this._valueFields.push(assignment);
3165
- this._fieldAssignments.set(fieldIndex, assignment);
3309
+ config = fieldNameOrConfig;
3310
+ }
3311
+ this._assertFieldExists(config.field);
3312
+ const resolvedAggregation = config.aggregation ?? 'sum';
3313
+ const resolvedName = config.name ?? `${this._aggregationLabel(resolvedAggregation)} of ${config.field}`;
3314
+ this._valueFields.push({
3315
+ field: config.field,
3316
+ aggregation: resolvedAggregation,
3317
+ name: resolvedName,
3318
+ numberFormat: config.numberFormat
3319
+ });
3166
3320
  return this;
3167
3321
  }
3168
- /**
3169
- * Add a field to the filter (page) area
3170
- * @param fieldName - Name of the source field (column header)
3171
- */ addFilterField(fieldName) {
3172
- const fieldIndex = this._cache.getFieldIndex(fieldName);
3173
- if (fieldIndex < 0) {
3174
- throw new Error(`Field not found in source data: ${fieldName}`);
3322
+ sortField(fieldName, order) {
3323
+ this._assertFieldExists(fieldName);
3324
+ if (!this._rowFields.includes(fieldName) && !this._columnFields.includes(fieldName)) {
3325
+ throw new Error(`Cannot sort field "${fieldName}": only row or column fields can be sorted`);
3175
3326
  }
3176
- const assignment = {
3177
- fieldName,
3178
- fieldIndex,
3179
- axis: 'filter'
3180
- };
3181
- this._filterFields.push(assignment);
3182
- this._fieldAssignments.set(fieldIndex, assignment);
3327
+ this._sortOrders.set(fieldName, order);
3183
3328
  return this;
3184
3329
  }
3185
- /**
3186
- * Set a sort order for a row or column field
3187
- * @param fieldName - Name of the field to sort
3188
- * @param order - Sort order ('asc' or 'desc')
3189
- */ sortField(fieldName, order) {
3190
- const fieldIndex = this._cache.getFieldIndex(fieldName);
3191
- if (fieldIndex < 0) {
3192
- throw new Error(`Field not found in source data: ${fieldName}`);
3193
- }
3194
- const assignment = this._fieldAssignments.get(fieldIndex);
3195
- if (!assignment) {
3196
- throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
3330
+ filterField(fieldName, filter) {
3331
+ this._assertFieldExists(fieldName);
3332
+ const hasInclude = 'include' in filter;
3333
+ const hasExclude = 'exclude' in filter;
3334
+ if (hasInclude && hasExclude || !hasInclude && !hasExclude) {
3335
+ throw new Error('Pivot filter must contain either include or exclude');
3197
3336
  }
3198
- if (assignment.axis !== 'row' && assignment.axis !== 'column') {
3199
- throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
3337
+ const values = hasInclude ? filter.include : filter.exclude;
3338
+ if (!values || values.length === 0) {
3339
+ throw new Error('Pivot filter values cannot be empty');
3200
3340
  }
3201
- assignment.sortOrder = order;
3341
+ this._filters.set(fieldName, filter);
3202
3342
  return this;
3203
3343
  }
3204
- /**
3205
- * Filter items for a row, column, or filter field
3206
- * @param fieldName - Name of the field to filter
3207
- * @param filter - Filter configuration with include or exclude list
3208
- */ filterField(fieldName, filter) {
3209
- const fieldIndex = this._cache.getFieldIndex(fieldName);
3210
- if (fieldIndex < 0) {
3211
- throw new Error(`Field not found in source data: ${fieldName}`);
3212
- }
3213
- const assignment = this._fieldAssignments.get(fieldIndex);
3214
- if (!assignment) {
3215
- throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
3216
- }
3217
- if (filter.include && filter.exclude) {
3218
- throw new Error('Cannot use both include and exclude in the same filter');
3219
- }
3220
- assignment.filter = filter;
3221
- return this;
3344
+ toPivotCacheDefinitionXml() {
3345
+ const cacheData = this._buildPivotCacheData();
3346
+ return this._buildPivotCacheDefinitionXml(cacheData);
3347
+ }
3348
+ toPivotCacheRecordsXml() {
3349
+ const cacheData = this._buildPivotCacheData();
3350
+ return this._buildPivotCacheRecordsXml(cacheData);
3351
+ }
3352
+ toPivotCacheDefinitionRelsXml() {
3353
+ const relsRoot = createElement('Relationships', {
3354
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
3355
+ }, [
3356
+ createElement('Relationship', {
3357
+ Id: 'rId1',
3358
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
3359
+ Target: `pivotCacheRecords${this._cachePartIndex}.xml`
3360
+ }, [])
3361
+ ]);
3362
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3363
+ relsRoot
3364
+ ])}`;
3222
3365
  }
3223
3366
  /**
3224
- * Generate the pivotTableDefinition XML
3225
- */ toXml() {
3367
+ * @internal
3368
+ */ buildPivotPartsXml() {
3369
+ const cacheData = this._buildPivotCacheData();
3370
+ return {
3371
+ cacheDefinitionXml: this._buildPivotCacheDefinitionXml(cacheData),
3372
+ cacheRecordsXml: this._buildPivotCacheRecordsXml(cacheData),
3373
+ cacheRelsXml: this.toPivotCacheDefinitionRelsXml(),
3374
+ pivotTableXml: this._buildPivotTableDefinitionXml(cacheData)
3375
+ };
3376
+ }
3377
+ toPivotTableDefinitionXml() {
3378
+ const cacheData = this._buildPivotCacheData();
3379
+ return this._buildPivotTableDefinitionXml(cacheData);
3380
+ }
3381
+ _buildPivotCacheDefinitionXml(cacheData) {
3382
+ const cacheFieldNodes = this._fields.map((field, index)=>this._buildCacheFieldNode(field, index, cacheData));
3383
+ const attrs = {
3384
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3385
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3386
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
3387
+ 'mc:Ignorable': 'xr',
3388
+ 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
3389
+ 'r:id': 'rId1',
3390
+ createdVersion: '8',
3391
+ minRefreshableVersion: '3',
3392
+ refreshedVersion: '8',
3393
+ refreshOnLoad: this._refreshOnLoad ? '1' : '0',
3394
+ recordCount: String(cacheData.rowCount)
3395
+ };
3396
+ const cacheSourceNode = createElement('cacheSource', {
3397
+ type: 'worksheet'
3398
+ }, [
3399
+ createElement('worksheetSource', {
3400
+ sheet: this._sourceSheetName,
3401
+ ref: toRange(this._sourceRange)
3402
+ }, [])
3403
+ ]);
3404
+ const cacheFieldsNode = createElement('cacheFields', {
3405
+ count: String(cacheFieldNodes.length)
3406
+ }, cacheFieldNodes);
3407
+ const extLstNode = createElement('extLst', {}, [
3408
+ createElement('ext', {
3409
+ uri: '{725AE2AE-9491-48be-B2B4-4EB974FC3084}',
3410
+ 'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'
3411
+ }, [
3412
+ createElement('x14:pivotCacheDefinition', {}, [])
3413
+ ])
3414
+ ]);
3415
+ const root = createElement('pivotCacheDefinition', attrs, [
3416
+ cacheSourceNode,
3417
+ cacheFieldsNode,
3418
+ extLstNode
3419
+ ]);
3420
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3421
+ root
3422
+ ])}`;
3423
+ }
3424
+ _buildPivotCacheRecordsXml(cacheData) {
3425
+ const recordNodes = cacheData.recordNodes;
3426
+ const root = createElement('pivotCacheRecords', {
3427
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3428
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3429
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
3430
+ 'mc:Ignorable': 'xr',
3431
+ 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
3432
+ count: String(recordNodes.length)
3433
+ }, recordNodes);
3434
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3435
+ root
3436
+ ])}`;
3437
+ }
3438
+ _buildPivotTableDefinitionXml(cacheData) {
3439
+ const effectiveValueFields = this._valueFields.length > 0 ? [
3440
+ this._valueFields[0]
3441
+ ] : [];
3442
+ const sourceFieldCount = this._fields.length;
3443
+ const pivotFields = [];
3444
+ const effectiveRowFieldName = this._rowFields[0];
3445
+ const rowFieldIndexes = effectiveRowFieldName ? [
3446
+ this._fieldIndex(effectiveRowFieldName)
3447
+ ] : [];
3448
+ const colFieldIndexes = this._columnFields.length > 0 ? [
3449
+ this._fieldIndex(this._columnFields[0])
3450
+ ] : [];
3451
+ const valueFieldIndexes = new Set(effectiveValueFields.map((valueField)=>this._fieldIndex(valueField.field)));
3452
+ for(let index = 0; index < this._fields.length; index++){
3453
+ const field = this._fields[index];
3454
+ const attrs = {
3455
+ showAll: '0'
3456
+ };
3457
+ if (rowFieldIndexes.includes(index)) {
3458
+ attrs.axis = 'axisRow';
3459
+ } else if (colFieldIndexes.includes(index)) {
3460
+ attrs.axis = 'axisCol';
3461
+ }
3462
+ if (valueFieldIndexes.has(index)) {
3463
+ attrs.dataField = '1';
3464
+ }
3465
+ const sortOrder = this._sortOrders.get(field.name);
3466
+ if (sortOrder) {
3467
+ attrs.sortType = SORT_TO_XML[sortOrder];
3468
+ }
3469
+ const children = [];
3470
+ if (rowFieldIndexes.includes(index) || colFieldIndexes.includes(index)) {
3471
+ const distinctItems = cacheData.distinctItemsByField[index] ?? [];
3472
+ const itemNodes = distinctItems.map((_item, itemIndex)=>createElement('item', {
3473
+ x: String(itemIndex)
3474
+ }, []));
3475
+ itemNodes.push(createElement('item', {
3476
+ t: 'default'
3477
+ }, []));
3478
+ children.push(createElement('items', {
3479
+ count: String(itemNodes.length)
3480
+ }, itemNodes));
3481
+ }
3482
+ pivotFields.push(createElement('pivotField', attrs, children));
3483
+ }
3226
3484
  const children = [];
3227
- // Calculate location (estimate based on fields)
3228
- const locationRef = this._calculateLocationRef();
3229
- // Calculate first data row/col offsets (1-based, relative to pivot table)
3230
- // firstHeaderRow: row offset of column headers (usually 1)
3231
- // firstDataRow: row offset where data starts (after filters and column headers)
3232
- // firstDataCol: column offset where data starts (after row labels)
3233
- const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;
3234
- const headerRows = this._columnFields.length > 0 ? 1 : 0;
3235
- const firstDataRow = filterRowCount + headerRows + 1;
3236
- const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;
3237
- const locationNode = createElement('location', {
3485
+ const locationRef = this._buildTargetAreaRef(cacheData);
3486
+ children.push(createElement('location', {
3238
3487
  ref: locationRef,
3239
- firstHeaderRow: String(filterRowCount + 1),
3240
- firstDataRow: String(firstDataRow),
3241
- firstDataCol: String(firstDataCol)
3242
- }, []);
3243
- children.push(locationNode);
3244
- // Build pivotFields (one per source field)
3245
- const pivotFieldNodes = [];
3246
- for (const cacheField of this._cache.fields){
3247
- const fieldNode = this._buildPivotFieldNode(cacheField.index);
3248
- pivotFieldNodes.push(fieldNode);
3249
- }
3488
+ firstHeaderRow: '1',
3489
+ firstDataRow: '1',
3490
+ firstDataCol: String(Math.max(1, this._rowFields.length + 1))
3491
+ }, []));
3250
3492
  children.push(createElement('pivotFields', {
3251
- count: String(pivotFieldNodes.length)
3252
- }, pivotFieldNodes));
3253
- // Row fields
3254
- if (this._rowFields.length > 0) {
3255
- const rowFieldNodes = this._rowFields.map((f)=>createElement('field', {
3256
- x: String(f.fieldIndex)
3257
- }, []));
3493
+ count: String(sourceFieldCount)
3494
+ }, pivotFields));
3495
+ if (rowFieldIndexes.length > 0) {
3258
3496
  children.push(createElement('rowFields', {
3259
- count: String(rowFieldNodes.length)
3260
- }, rowFieldNodes));
3261
- // Row items
3262
- const rowItemNodes = this._buildRowItems();
3497
+ count: String(rowFieldIndexes.length)
3498
+ }, rowFieldIndexes.map((fieldIndex)=>createElement('field', {
3499
+ x: String(fieldIndex)
3500
+ }, []))));
3501
+ const distinctRowItems = cacheData.distinctItemsByField[rowFieldIndexes[0]] ?? [];
3502
+ const rowItemNodes = [];
3503
+ if (distinctRowItems.length > 0) {
3504
+ rowItemNodes.push(createElement('i', {}, [
3505
+ createElement('x', {}, [])
3506
+ ]));
3507
+ for(let itemIndex = 1; itemIndex < distinctRowItems.length; itemIndex++){
3508
+ rowItemNodes.push(createElement('i', {}, [
3509
+ createElement('x', {
3510
+ v: String(itemIndex)
3511
+ }, [])
3512
+ ]));
3513
+ }
3514
+ }
3515
+ rowItemNodes.push(createElement('i', {
3516
+ t: 'grand'
3517
+ }, [
3518
+ createElement('x', {}, [])
3519
+ ]));
3263
3520
  children.push(createElement('rowItems', {
3264
3521
  count: String(rowItemNodes.length)
3265
3522
  }, rowItemNodes));
3266
3523
  }
3267
- // Column fields
3268
- if (this._columnFields.length > 0) {
3269
- const colFieldNodes = this._columnFields.map((f)=>createElement('field', {
3270
- x: String(f.fieldIndex)
3271
- }, []));
3272
- // If we have multiple value fields, add -2 to indicate where "Values" header goes
3273
- if (this._valueFields.length > 1) {
3274
- colFieldNodes.push(createElement('field', {
3275
- x: '-2'
3276
- }, []));
3277
- }
3278
- children.push(createElement('colFields', {
3279
- count: String(colFieldNodes.length)
3280
- }, colFieldNodes));
3281
- // Column items - need to account for multiple value fields
3282
- const colItemNodes = this._buildColItems();
3283
- children.push(createElement('colItems', {
3284
- count: String(colItemNodes.length)
3285
- }, colItemNodes));
3286
- } else if (this._valueFields.length > 1) {
3287
- // If no column fields but we have multiple values, need colFields with -2 (data field indicator)
3524
+ if (colFieldIndexes.length > 0) {
3288
3525
  children.push(createElement('colFields', {
3289
- count: '1'
3290
- }, [
3291
- createElement('field', {
3292
- x: '-2'
3293
- }, [])
3294
- ]));
3295
- // Column items for each value field
3296
- const colItemNodes = [];
3297
- for(let i = 0; i < this._valueFields.length; i++){
3298
- colItemNodes.push(createElement('i', {}, [
3299
- createElement('x', i === 0 ? {} : {
3300
- v: String(i)
3301
- }, [])
3302
- ]));
3303
- }
3304
- children.push(createElement('colItems', {
3305
- count: String(colItemNodes.length)
3306
- }, colItemNodes));
3307
- } else if (this._valueFields.length === 1) {
3308
- // Single value field - just add a single column item
3309
- children.push(createElement('colItems', {
3310
- count: '1'
3311
- }, [
3312
- createElement('i', {}, [])
3313
- ]));
3526
+ count: String(colFieldIndexes.length)
3527
+ }, colFieldIndexes.map((fieldIndex)=>createElement('field', {
3528
+ x: String(fieldIndex)
3529
+ }, []))));
3314
3530
  }
3315
- // Page (filter) fields
3531
+ // Excel expects colItems even when no explicit column fields are configured.
3532
+ children.push(createElement('colItems', {
3533
+ count: '1'
3534
+ }, [
3535
+ createElement('i', {}, [])
3536
+ ]));
3316
3537
  if (this._filterFields.length > 0) {
3317
- const pageFieldNodes = this._filterFields.map((f)=>createElement('pageField', {
3318
- fld: String(f.fieldIndex),
3319
- hier: '-1'
3320
- }, []));
3321
3538
  children.push(createElement('pageFields', {
3322
- count: String(pageFieldNodes.length)
3323
- }, pageFieldNodes));
3324
- }
3325
- // Data fields (values)
3326
- if (this._valueFields.length > 0) {
3327
- const dataFieldNodes = this._valueFields.map((f)=>{
3539
+ count: String(this._filterFields.length)
3540
+ }, this._filterFields.map((field, index)=>createElement('pageField', {
3541
+ fld: String(this._fieldIndex(field)),
3542
+ hier: '-1',
3543
+ item: String(index)
3544
+ }, []))));
3545
+ }
3546
+ if (effectiveValueFields.length > 0) {
3547
+ children.push(createElement('dataFields', {
3548
+ count: String(effectiveValueFields.length)
3549
+ }, effectiveValueFields.map((valueField)=>{
3328
3550
  const attrs = {
3329
- name: f.displayName || f.fieldName,
3330
- fld: String(f.fieldIndex),
3551
+ name: valueField.name,
3552
+ fld: String(this._fieldIndex(valueField.field)),
3331
3553
  baseField: '0',
3332
3554
  baseItem: '0',
3333
- subtotal: f.aggregation || 'sum'
3555
+ subtotal: AGGREGATION_TO_XML[valueField.aggregation]
3334
3556
  };
3335
- if (f.numFmtId !== undefined) {
3336
- attrs.numFmtId = String(f.numFmtId);
3557
+ if (valueField.numberFormat) {
3558
+ attrs.numFmtId = String(this._workbook.styles.getOrCreateNumFmtId(valueField.numberFormat));
3337
3559
  }
3338
3560
  return createElement('dataField', attrs, []);
3339
- });
3340
- children.push(createElement('dataFields', {
3341
- count: String(dataFieldNodes.length)
3342
- }, dataFieldNodes));
3561
+ })));
3343
3562
  }
3344
- // Pivot table style
3345
3563
  children.push(createElement('pivotTableStyleInfo', {
3346
3564
  name: 'PivotStyleMedium9',
3347
3565
  showRowHeaders: '1',
@@ -3350,672 +3568,400 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
3350
3568
  showColStripes: '0',
3351
3569
  showLastColumn: '1'
3352
3570
  }, []));
3353
- const pivotTableNode = createElement('pivotTableDefinition', {
3571
+ const attrs = {
3354
3572
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3355
3573
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3356
3574
  name: this._name,
3357
- cacheId: String(this._cache.cacheId),
3575
+ cacheId: String(this._cacheId),
3576
+ dataCaption: 'Values',
3358
3577
  applyNumberFormats: '1',
3359
3578
  applyBorderFormats: '0',
3360
3579
  applyFontFormats: '0',
3361
3580
  applyPatternFormats: '0',
3362
3581
  applyAlignmentFormats: '0',
3363
3582
  applyWidthHeightFormats: '1',
3364
- dataCaption: 'Values',
3365
3583
  updatedVersion: '8',
3366
3584
  minRefreshableVersion: '3',
3585
+ createdVersion: '8',
3367
3586
  useAutoFormatting: '1',
3368
3587
  rowGrandTotals: '1',
3369
3588
  colGrandTotals: '1',
3370
3589
  itemPrintTitles: '1',
3371
- createdVersion: '8',
3372
3590
  indent: '0',
3591
+ multipleFieldFilters: this._filters.size > 0 ? '1' : '0',
3373
3592
  outline: '1',
3374
- outlineData: '1',
3375
- multipleFieldFilters: '0'
3376
- }, children);
3593
+ outlineData: '1'
3594
+ };
3595
+ const root = createElement('pivotTableDefinition', attrs, children);
3377
3596
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3378
- pivotTableNode
3597
+ root
3379
3598
  ])}`;
3380
3599
  }
3381
- /**
3382
- * Build a pivotField node for a given field index
3383
- */ _buildPivotFieldNode(fieldIndex) {
3384
- const attrs = {};
3385
- const children = [];
3386
- // Check if this field is assigned to an axis
3387
- const rowField = this._rowFields.find((f)=>f.fieldIndex === fieldIndex);
3388
- const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
3389
- const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
3390
- const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
3391
- // Get the assignment to check for sort/filter options
3392
- const assignment = rowField || colField || filterField;
3393
- if (rowField) {
3394
- attrs.axis = 'axisRow';
3395
- attrs.showAll = '0';
3396
- // Add sort order if specified
3397
- if (rowField.sortOrder) {
3398
- attrs.sortType = rowField.sortOrder === 'asc' ? 'ascending' : 'descending';
3399
- }
3400
- // Add items for shared values
3401
- const cacheField = this._cache.fields[fieldIndex];
3402
- if (cacheField && cacheField.sharedItems.length > 0) {
3403
- const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
3404
- children.push(createElement('items', {
3405
- count: String(itemNodes.length)
3406
- }, itemNodes));
3407
- }
3408
- } else if (colField) {
3409
- attrs.axis = 'axisCol';
3410
- attrs.showAll = '0';
3411
- // Add sort order if specified
3412
- if (colField.sortOrder) {
3413
- attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
3600
+ _buildCacheFieldNode(field, fieldIndex, cacheData) {
3601
+ const info = cacheData.numericInfoByField[fieldIndex];
3602
+ const isAxisField = cacheData.isAxisFieldByIndex[fieldIndex];
3603
+ const isValueField = cacheData.isValueFieldByIndex[fieldIndex];
3604
+ const allNonNullAreNumbers = info.nonNullCount > 0 && info.numericCount === info.nonNullCount;
3605
+ if (isValueField || !isAxisField && allNonNullAreNumbers) {
3606
+ const minValue = info.hasNumeric ? info.min : 0;
3607
+ const maxValue = info.hasNumeric ? info.max : 0;
3608
+ const hasInteger = info.hasNumeric ? info.allIntegers : true;
3609
+ const attrs = {
3610
+ containsSemiMixedTypes: '0',
3611
+ containsString: '0',
3612
+ containsNumber: '1',
3613
+ minValue: String(minValue),
3614
+ maxValue: String(maxValue)
3615
+ };
3616
+ if (hasInteger) {
3617
+ attrs.containsInteger = '1';
3414
3618
  }
3415
- const cacheField = this._cache.fields[fieldIndex];
3416
- if (cacheField && cacheField.sharedItems.length > 0) {
3417
- const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
3418
- children.push(createElement('items', {
3419
- count: String(itemNodes.length)
3420
- }, itemNodes));
3421
- }
3422
- } else if (filterField) {
3423
- attrs.axis = 'axisPage';
3424
- attrs.showAll = '0';
3425
- const cacheField = this._cache.fields[fieldIndex];
3426
- if (cacheField && cacheField.sharedItems.length > 0) {
3427
- const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
3428
- children.push(createElement('items', {
3429
- count: String(itemNodes.length)
3430
- }, itemNodes));
3431
- }
3432
- } else if (valueField) {
3433
- attrs.dataField = '1';
3434
- attrs.showAll = '0';
3435
- } else {
3436
- attrs.showAll = '0';
3619
+ return createElement('cacheField', {
3620
+ name: field.name,
3621
+ numFmtId: '0'
3622
+ }, [
3623
+ createElement('sharedItems', attrs, [])
3624
+ ]);
3625
+ }
3626
+ if (!isAxisField) {
3627
+ return createElement('cacheField', {
3628
+ name: field.name,
3629
+ numFmtId: '0'
3630
+ }, [
3631
+ createElement('sharedItems', {}, [])
3632
+ ]);
3437
3633
  }
3438
- return createElement('pivotField', attrs, children);
3634
+ const sharedItems = cacheData.sharedItemsByField[fieldIndex] ?? [];
3635
+ return createElement('cacheField', {
3636
+ name: field.name,
3637
+ numFmtId: '0'
3638
+ }, [
3639
+ createElement('sharedItems', {
3640
+ count: String(sharedItems.length)
3641
+ }, sharedItems)
3642
+ ]);
3439
3643
  }
3440
- /**
3441
- * Build item nodes for a pivot field, with optional filtering
3442
- */ _buildItemNodes(sharedItems, filter) {
3443
- const itemNodes = [];
3444
- for(let i = 0; i < sharedItems.length; i++){
3445
- const itemValue = sharedItems[i];
3446
- const itemAttrs = {
3447
- x: String(i)
3448
- };
3449
- // Check if this item should be hidden
3450
- if (filter) {
3451
- let hidden = false;
3452
- if (filter.exclude && filter.exclude.includes(itemValue)) {
3453
- hidden = true;
3454
- } else if (filter.include && !filter.include.includes(itemValue)) {
3455
- hidden = true;
3456
- }
3457
- if (hidden) {
3458
- itemAttrs.h = '1';
3459
- }
3644
+ _buildTargetAreaRef(cacheData) {
3645
+ const start = this._targetCell;
3646
+ const estimatedRows = Math.max(3, this._estimateOutputRows(cacheData));
3647
+ const estimatedCols = Math.max(1, this._rowFields.length + Math.max(1, this._valueFields.length));
3648
+ const endRow = start.row + estimatedRows - 1;
3649
+ const endCol = start.col + estimatedCols - 1;
3650
+ return `${toAddress(start.row, start.col)}:${toAddress(endRow, endCol)}`;
3651
+ }
3652
+ _estimateOutputRows(cacheData) {
3653
+ if (this._rowFields.length === 0) {
3654
+ return 3;
3655
+ }
3656
+ const rowFieldIndex = this._fieldIndex(this._rowFields[0]);
3657
+ const distinctItems = cacheData.distinctItemsByField[rowFieldIndex] ?? [];
3658
+ return Math.max(3, distinctItems.length + 2);
3659
+ }
3660
+ _buildPivotCacheData() {
3661
+ const rowCount = Math.max(0, this._sourceRange.end.row - this._sourceRange.start.row);
3662
+ const fieldCount = this._fields.length;
3663
+ const recordNodes = new Array(rowCount);
3664
+ const sharedItemIndexByField = new Array(fieldCount).fill(null);
3665
+ const sharedItemsByField = new Array(fieldCount).fill(null);
3666
+ const distinctItemsByField = new Array(fieldCount).fill(null);
3667
+ const numericInfoByField = new Array(fieldCount);
3668
+ const isAxisFieldByIndex = new Array(fieldCount);
3669
+ const isValueFieldByIndex = new Array(fieldCount);
3670
+ const effectiveRowField = this._rowFields[0] ?? null;
3671
+ const effectiveColumnField = this._columnFields[0] ?? null;
3672
+ const filterFields = new Set(this._filterFields);
3673
+ const valueFields = new Set(this._valueFields.map((valueField)=>valueField.field));
3674
+ for(let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++){
3675
+ const fieldName = this._fields[fieldIndex].name;
3676
+ const isAxisField = fieldName === effectiveRowField || fieldName === effectiveColumnField || filterFields.has(fieldName);
3677
+ const isValueField = valueFields.has(fieldName);
3678
+ isAxisFieldByIndex[fieldIndex] = isAxisField;
3679
+ isValueFieldByIndex[fieldIndex] = isValueField;
3680
+ if (isAxisField) {
3681
+ sharedItemIndexByField[fieldIndex] = new Map();
3682
+ sharedItemsByField[fieldIndex] = [];
3683
+ distinctItemsByField[fieldIndex] = [];
3460
3684
  }
3461
- itemNodes.push(createElement('item', itemAttrs, []));
3685
+ numericInfoByField[fieldIndex] = {
3686
+ nonNullCount: 0,
3687
+ numericCount: 0,
3688
+ min: 0,
3689
+ max: 0,
3690
+ hasNumeric: false,
3691
+ allIntegers: true
3692
+ };
3462
3693
  }
3463
- // Add default subtotal item
3464
- itemNodes.push(createElement('item', {
3465
- t: 'default'
3466
- }, []));
3467
- return itemNodes;
3468
- }
3469
- /**
3470
- * Build row items based on unique values in row fields
3471
- */ _buildRowItems() {
3472
- const items = [];
3473
- if (this._rowFields.length === 0) return items;
3474
- // Get unique values from first row field
3475
- const firstRowField = this._rowFields[0];
3476
- const cacheField = this._cache.fields[firstRowField.fieldIndex];
3477
- if (cacheField && cacheField.sharedItems.length > 0) {
3478
- for(let i = 0; i < cacheField.sharedItems.length; i++){
3479
- items.push(createElement('i', {}, [
3480
- createElement('x', i === 0 ? {} : {
3481
- v: String(i)
3482
- }, [])
3483
- ]));
3694
+ for(let rowOffset = 0; rowOffset < rowCount; rowOffset++){
3695
+ const row = this._sourceRange.start.row + 1 + rowOffset;
3696
+ const valueNodes = [];
3697
+ for(let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++){
3698
+ const field = this._fields[fieldIndex];
3699
+ const cellValue = this._sourceSheet.getCellIfExists(row, field.sourceCol)?.value ?? null;
3700
+ if (cellValue !== null) {
3701
+ const numericInfo = numericInfoByField[fieldIndex];
3702
+ numericInfo.nonNullCount++;
3703
+ if (typeof cellValue === 'number' && Number.isFinite(cellValue)) {
3704
+ numericInfo.numericCount++;
3705
+ if (!numericInfo.hasNumeric) {
3706
+ numericInfo.min = cellValue;
3707
+ numericInfo.max = cellValue;
3708
+ numericInfo.hasNumeric = true;
3709
+ } else {
3710
+ if (cellValue < numericInfo.min) numericInfo.min = cellValue;
3711
+ if (cellValue > numericInfo.max) numericInfo.max = cellValue;
3712
+ }
3713
+ if (!Number.isInteger(cellValue)) {
3714
+ numericInfo.allIntegers = false;
3715
+ }
3716
+ }
3717
+ if (isAxisFieldByIndex[fieldIndex]) {
3718
+ const distinctMap = sharedItemIndexByField[fieldIndex];
3719
+ const key = this._distinctKey(cellValue);
3720
+ let index = distinctMap.get(key);
3721
+ if (index === undefined) {
3722
+ index = distinctMap.size;
3723
+ distinctMap.set(key, index);
3724
+ distinctItemsByField[fieldIndex].push(cellValue);
3725
+ const sharedNode = this._buildSharedItemNode(cellValue);
3726
+ if (sharedNode) {
3727
+ sharedItemsByField[fieldIndex].push(sharedNode);
3728
+ }
3729
+ }
3730
+ valueNodes.push(createElement('x', {
3731
+ v: String(index)
3732
+ }, []));
3733
+ continue;
3734
+ }
3735
+ }
3736
+ valueNodes.push(this._buildRawCacheValueNode(cellValue));
3484
3737
  }
3738
+ recordNodes[rowOffset] = createElement('r', {}, valueNodes);
3485
3739
  }
3486
- // Add grand total row
3487
- items.push(createElement('i', {
3488
- t: 'grand'
3489
- }, [
3490
- createElement('x', {}, [])
3491
- ]));
3492
- return items;
3493
- }
3494
- /**
3495
- * Build column items based on unique values in column fields
3496
- */ _buildColItems() {
3497
- const items = [];
3498
- if (this._columnFields.length === 0) return items;
3499
- // Get unique values from first column field
3500
- const firstColField = this._columnFields[0];
3501
- const cacheField = this._cache.fields[firstColField.fieldIndex];
3502
- if (cacheField && cacheField.sharedItems.length > 0) {
3503
- if (this._valueFields.length > 1) {
3504
- // Multiple value fields - need nested items for each column value + value field combination
3505
- for(let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++){
3506
- for(let valIdx = 0; valIdx < this._valueFields.length; valIdx++){
3507
- const xNodes = [
3508
- createElement('x', colIdx === 0 ? {} : {
3509
- v: String(colIdx)
3510
- }, []),
3511
- createElement('x', valIdx === 0 ? {} : {
3512
- v: String(valIdx)
3513
- }, [])
3514
- ];
3515
- items.push(createElement('i', {}, xNodes));
3516
- }
3740
+ return {
3741
+ rowCount,
3742
+ recordNodes,
3743
+ sharedItemIndexByField,
3744
+ sharedItemsByField,
3745
+ distinctItemsByField,
3746
+ numericInfoByField,
3747
+ isAxisFieldByIndex,
3748
+ isValueFieldByIndex
3749
+ };
3750
+ }
3751
+ _buildSharedItemNode(value) {
3752
+ if (typeof value === 'string') {
3753
+ return {
3754
+ s: [],
3755
+ ':@': {
3756
+ '@_v': value
3517
3757
  }
3518
- } else {
3519
- // Single value field - simple column items
3520
- for(let i = 0; i < cacheField.sharedItems.length; i++){
3521
- items.push(createElement('i', {}, [
3522
- createElement('x', i === 0 ? {} : {
3523
- v: String(i)
3524
- }, [])
3525
- ]));
3758
+ };
3759
+ }
3760
+ if (typeof value === 'number') {
3761
+ return createElement('n', {
3762
+ v: String(value)
3763
+ }, []);
3764
+ }
3765
+ if (typeof value === 'boolean') {
3766
+ return createElement('b', {
3767
+ v: value ? '1' : '0'
3768
+ }, []);
3769
+ }
3770
+ if (value instanceof Date) {
3771
+ return createElement('d', {
3772
+ v: value.toISOString()
3773
+ }, []);
3774
+ }
3775
+ return null;
3776
+ }
3777
+ _buildRawCacheValueNode(value) {
3778
+ if (value === null) {
3779
+ return createElement('m', {}, []);
3780
+ }
3781
+ if (typeof value === 'string') {
3782
+ return {
3783
+ s: [],
3784
+ ':@': {
3785
+ '@_v': value
3526
3786
  }
3527
- }
3787
+ };
3528
3788
  }
3529
- // Add grand total column(s)
3530
- if (this._valueFields.length > 1) {
3531
- // Grand total for each value field
3532
- for(let valIdx = 0; valIdx < this._valueFields.length; valIdx++){
3533
- const xNodes = [
3534
- createElement('x', {}, []),
3535
- createElement('x', valIdx === 0 ? {} : {
3536
- v: String(valIdx)
3537
- }, [])
3538
- ];
3539
- items.push(createElement('i', {
3540
- t: 'grand'
3541
- }, xNodes));
3542
- }
3543
- } else {
3544
- items.push(createElement('i', {
3545
- t: 'grand'
3546
- }, [
3547
- createElement('x', {}, [])
3548
- ]));
3789
+ if (typeof value === 'number') {
3790
+ return createElement('n', {
3791
+ v: String(value)
3792
+ }, []);
3549
3793
  }
3550
- return items;
3551
- }
3552
- /**
3553
- * Calculate the location reference for the pivot table output
3554
- */ _calculateLocationRef() {
3555
- // Estimate output size based on fields
3556
- const numRows = this._estimateRowCount();
3557
- const numCols = this._estimateColCount();
3558
- const startRow = this._targetRow;
3559
- const startCol = this._targetCol;
3560
- const endRow = startRow + numRows - 1;
3561
- const endCol = startCol + numCols - 1;
3562
- return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;
3563
- }
3564
- /**
3565
- * Estimate number of rows in pivot table output
3566
- */ _estimateRowCount() {
3567
- let count = 1; // Header row
3568
- // Add filter area rows
3569
- count += this._filterFields.length;
3570
- // Add row labels (unique values in row fields)
3571
- if (this._rowFields.length > 0) {
3572
- const firstRowField = this._rowFields[0];
3573
- const cacheField = this._cache.fields[firstRowField.fieldIndex];
3574
- count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
3575
- } else {
3576
- count += 1; // At least one data row
3794
+ if (typeof value === 'boolean') {
3795
+ return createElement('b', {
3796
+ v: value ? '1' : '0'
3797
+ }, []);
3798
+ }
3799
+ if (value instanceof Date) {
3800
+ return createElement('d', {
3801
+ v: value.toISOString()
3802
+ }, []);
3577
3803
  }
3578
- return Math.max(count, 3);
3804
+ return createElement('m', {}, []);
3579
3805
  }
3580
- /**
3581
- * Estimate number of columns in pivot table output
3582
- */ _estimateColCount() {
3583
- let count = 0;
3584
- // Row label columns
3585
- count += Math.max(this._rowFields.length, 1);
3586
- // Column labels (unique values in column fields)
3587
- if (this._columnFields.length > 0) {
3588
- const firstColField = this._columnFields[0];
3589
- const cacheField = this._cache.fields[firstColField.fieldIndex];
3590
- count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
3591
- } else {
3592
- // Value columns
3593
- count += Math.max(this._valueFields.length, 1);
3806
+ _assertFieldExists(fieldName) {
3807
+ if (!this._fields.some((field)=>field.name === fieldName)) {
3808
+ throw new Error(`Pivot field not found: ${fieldName}`);
3594
3809
  }
3595
- return Math.max(count, 2);
3596
3810
  }
3597
- /**
3598
- * Convert 0-based column index to letter (A, B, ..., Z, AA, etc.)
3599
- */ _colToLetter(col) {
3600
- let result = '';
3601
- let n = col;
3602
- while(n >= 0){
3603
- result = String.fromCharCode(n % 26 + 65) + result;
3604
- n = Math.floor(n / 26) - 1;
3811
+ _fieldIndex(fieldName) {
3812
+ const index = this._fields.findIndex((field)=>field.name === fieldName);
3813
+ if (index < 0) {
3814
+ throw new Error(`Pivot field not found: ${fieldName}`);
3605
3815
  }
3606
- return result;
3816
+ return index;
3817
+ }
3818
+ _aggregationLabel(aggregation) {
3819
+ switch(aggregation){
3820
+ case 'sum':
3821
+ return 'Sum';
3822
+ case 'count':
3823
+ return 'Count';
3824
+ case 'average':
3825
+ return 'Average';
3826
+ case 'min':
3827
+ return 'Min';
3828
+ case 'max':
3829
+ return 'Max';
3830
+ }
3831
+ }
3832
+ _distinctKey(value) {
3833
+ if (value instanceof Date) {
3834
+ return `d:${value.toISOString()}`;
3835
+ }
3836
+ if (typeof value === 'string') {
3837
+ return `s:${value}`;
3838
+ }
3839
+ if (typeof value === 'number') {
3840
+ return `n:${value}`;
3841
+ }
3842
+ if (typeof value === 'boolean') {
3843
+ return `b:${value ? 1 : 0}`;
3844
+ }
3845
+ if (typeof value === 'object' && value && 'error' in value) {
3846
+ return `e:${value.error}`;
3847
+ }
3848
+ return 'u:';
3607
3849
  }
3608
3850
  }
3609
3851
 
3610
- /**
3611
- * Manages the pivot cache (definition and records) for a pivot table.
3612
- * The cache stores source data metadata and cached values.
3613
- */ class PivotCache {
3614
- constructor(cacheId, sourceSheet, sourceRange, fileIndex){
3615
- this._fields = [];
3616
- this._records = [];
3617
- this._recordCount = 0;
3618
- this._saveData = true;
3619
- this._refreshOnLoad = true; // Default to true
3620
- // Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
3621
- this._sharedItemsIndexMap = new Map();
3622
- this._blankItemIndexMap = new Map();
3623
- this._styles = null;
3624
- this._cacheId = cacheId;
3625
- this._fileIndex = fileIndex;
3626
- this._sourceSheet = sourceSheet;
3627
- this._sourceRange = sourceRange;
3852
+ class EagerZipStore {
3853
+ constructor(files){
3854
+ this._files = files;
3628
3855
  }
3629
- /**
3630
- * Set styles reference for number format resolution.
3631
- * @internal
3632
- */ setStyles(styles) {
3633
- this._styles = styles;
3856
+ get(path) {
3857
+ return this._files.get(path);
3634
3858
  }
3635
- /**
3636
- * Get the cache ID
3637
- */ get cacheId() {
3638
- return this._cacheId;
3859
+ set(path, content) {
3860
+ this._files.set(path, content);
3639
3861
  }
3640
- /**
3641
- * Get the file index for this cache (used for file naming).
3642
- */ get fileIndex() {
3643
- return this._fileIndex;
3862
+ has(path) {
3863
+ return this._files.has(path);
3644
3864
  }
3645
- /**
3646
- * Set refreshOnLoad option
3647
- */ set refreshOnLoad(value) {
3648
- this._refreshOnLoad = value;
3865
+ delete(path) {
3866
+ this._files.delete(path);
3649
3867
  }
3650
- /**
3651
- * Set saveData option
3652
- */ set saveData(value) {
3653
- this._saveData = value;
3868
+ getText(path) {
3869
+ const data = this._files.get(path);
3870
+ if (!data) return undefined;
3871
+ return fflate.strFromU8(data);
3654
3872
  }
3655
- /**
3656
- * Get refreshOnLoad option
3657
- */ get refreshOnLoad() {
3658
- return this._refreshOnLoad;
3659
- }
3660
- /**
3661
- * Get saveData option
3662
- */ get saveData() {
3663
- return this._saveData;
3664
- }
3665
- /**
3666
- * Get the source sheet name
3667
- */ get sourceSheet() {
3668
- return this._sourceSheet;
3669
- }
3670
- /**
3671
- * Get the source range
3672
- */ get sourceRange() {
3673
- return this._sourceRange;
3674
- }
3675
- /**
3676
- * Get the full source reference (Sheet!Range)
3677
- */ get sourceRef() {
3678
- return `${this._sourceSheet}!${this._sourceRange}`;
3873
+ setText(path, content) {
3874
+ this._files.set(path, fflate.strToU8(content));
3679
3875
  }
3680
- /**
3681
- * Get the fields in this cache
3682
- */ get fields() {
3683
- return this._fields;
3684
- }
3685
- /**
3686
- * Get the number of data records
3687
- */ get recordCount() {
3688
- return this._recordCount;
3876
+ toFiles() {
3877
+ return Promise.resolve(this._files);
3689
3878
  }
3690
- /**
3691
- * Build the cache from source data.
3692
- * @param headers - Array of column header names
3693
- * @param data - 2D array of data rows (excluding headers)
3694
- */ buildFromData(headers, data) {
3695
- this._recordCount = data.length;
3696
- // Initialize fields from headers
3697
- this._fields = headers.map((name, index)=>({
3698
- name,
3699
- index,
3700
- isNumeric: true,
3701
- isDate: false,
3702
- hasBoolean: false,
3703
- hasBlank: false,
3704
- numFmtId: undefined,
3705
- sharedItems: [],
3706
- minValue: undefined,
3707
- maxValue: undefined,
3708
- minDate: undefined,
3709
- maxDate: undefined
3710
- }));
3711
- // Use Maps for unique value collection during analysis
3712
- const sharedItemsMaps = this._fields.map(()=>new Map());
3713
- // Analyze data to determine field types and collect unique values
3714
- for (const row of data){
3715
- for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
3716
- const value = row[colIdx];
3717
- const field = this._fields[colIdx];
3718
- if (value === null || value === undefined) {
3719
- field.hasBlank = true;
3720
- continue;
3721
- }
3722
- if (typeof value === 'string') {
3723
- field.isNumeric = false;
3724
- const map = sharedItemsMaps[colIdx];
3725
- if (!map.has(value)) {
3726
- map.set(value, value);
3727
- }
3728
- } else if (typeof value === 'number') {
3729
- if (field.isDate) {
3730
- const d = this._excelSerialToDate(value);
3731
- if (!field.minDate || d < field.minDate) {
3732
- field.minDate = d;
3733
- }
3734
- if (!field.maxDate || d > field.maxDate) {
3735
- field.maxDate = d;
3736
- }
3737
- } else {
3738
- if (field.minValue === undefined || value < field.minValue) {
3739
- field.minValue = value;
3740
- }
3741
- if (field.maxValue === undefined || value > field.maxValue) {
3742
- field.maxValue = value;
3743
- }
3744
- }
3745
- } else if (value instanceof Date) {
3746
- field.isDate = true;
3747
- field.isNumeric = false;
3748
- if (!field.minDate || value < field.minDate) {
3749
- field.minDate = value;
3750
- }
3751
- if (!field.maxDate || value > field.maxDate) {
3752
- field.maxDate = value;
3753
- }
3754
- } else if (typeof value === 'boolean') {
3755
- field.isNumeric = false;
3756
- field.hasBoolean = true;
3757
- }
3758
- }
3759
- }
3760
- // Resolve number formats if styles are available
3761
- if (this._styles) {
3762
- const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
3763
- for (const field of this._fields){
3764
- if (field.isDate) {
3765
- field.numFmtId = dateFmtId;
3766
- }
3767
- }
3768
- }
3769
- // Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
3770
- this._sharedItemsIndexMap.clear();
3771
- this._blankItemIndexMap.clear();
3772
- for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
3773
- const field = this._fields[colIdx];
3774
- const map = sharedItemsMaps[colIdx];
3775
- // Convert Map values to array (maintains insertion order in ES6+)
3776
- field.sharedItems = Array.from(map.values());
3777
- // Build reverse lookup Map: value -> index
3778
- if (field.sharedItems.length > 0) {
3779
- const indexMap = new Map();
3780
- for(let i = 0; i < field.sharedItems.length; i++){
3781
- indexMap.set(field.sharedItems[i], i);
3782
- }
3783
- this._sharedItemsIndexMap.set(colIdx, indexMap);
3784
- if (field.hasBlank) {
3785
- const blankIndex = field.sharedItems.length;
3786
- this._blankItemIndexMap.set(colIdx, blankIndex);
3787
- }
3788
- }
3879
+ }
3880
+ class LazyZipStore {
3881
+ constructor(data){
3882
+ this._files = new Map();
3883
+ this._deleted = new Set();
3884
+ this._entryNames = null;
3885
+ this._data = data;
3886
+ }
3887
+ get(path) {
3888
+ if (this._deleted.has(path)) return undefined;
3889
+ const cached = this._files.get(path);
3890
+ if (cached) return cached;
3891
+ this._ensureIndex();
3892
+ if (this._entryNames && !this._entryNames.has(path)) return undefined;
3893
+ const result = fflate.unzipSync(this._data, {
3894
+ filter: (file)=>file.name === path
3895
+ });
3896
+ const data = result[path];
3897
+ if (data) {
3898
+ this._files.set(path, data);
3789
3899
  }
3790
- // Store records
3791
- this._records = data;
3900
+ return data;
3792
3901
  }
3793
- /**
3794
- * Get field by name
3795
- */ getField(name) {
3796
- return this._fields.find((f)=>f.name === name);
3902
+ set(path, content) {
3903
+ this._files.set(path, content);
3904
+ this._deleted.delete(path);
3905
+ if (this._entryNames) {
3906
+ this._entryNames.add(path);
3907
+ }
3797
3908
  }
3798
- /**
3799
- * Get field index by name
3800
- */ getFieldIndex(name) {
3801
- const field = this._fields.find((f)=>f.name === name);
3802
- return field ? field.index : -1;
3909
+ has(path) {
3910
+ if (this._deleted.has(path)) return false;
3911
+ if (this._files.has(path)) return true;
3912
+ this._ensureIndex();
3913
+ return this._entryNames?.has(path) ?? false;
3803
3914
  }
3804
- /**
3805
- * Generate the pivotCacheDefinition XML
3806
- */ toDefinitionXml(recordsRelId) {
3807
- const cacheFieldNodes = this._fields.map((field)=>{
3808
- const sharedItemsAttrs = {};
3809
- const sharedItemChildren = [];
3810
- if (field.sharedItems.length > 0) {
3811
- // String field with shared items
3812
- const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
3813
- sharedItemsAttrs.count = String(total);
3814
- sharedItemsAttrs.containsString = '1';
3815
- if (field.hasBlank) {
3816
- sharedItemsAttrs.containsBlank = '1';
3817
- }
3818
- for (const item of field.sharedItems){
3819
- sharedItemChildren.push(createElement('s', {
3820
- v: item
3821
- }, []));
3822
- }
3823
- if (field.hasBlank) {
3824
- sharedItemChildren.push(createElement('m', {}, []));
3825
- }
3826
- } else if (field.isDate) {
3827
- sharedItemsAttrs.containsSemiMixedTypes = '0';
3828
- sharedItemsAttrs.containsString = '0';
3829
- sharedItemsAttrs.containsDate = '1';
3830
- sharedItemsAttrs.containsNonDate = '0';
3831
- if (field.hasBlank) {
3832
- sharedItemsAttrs.containsBlank = '1';
3833
- }
3834
- if (field.minDate) {
3835
- sharedItemsAttrs.minDate = this._formatDate(field.minDate);
3836
- }
3837
- if (field.maxDate) {
3838
- const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
3839
- sharedItemsAttrs.maxDate = this._formatDate(maxDate);
3840
- }
3841
- } else if (field.isNumeric) {
3842
- // Numeric field - use "0"/"1" for boolean attributes as Excel expects
3843
- sharedItemsAttrs.containsSemiMixedTypes = '0';
3844
- sharedItemsAttrs.containsString = '0';
3845
- sharedItemsAttrs.containsNumber = '1';
3846
- if (field.hasBlank) {
3847
- sharedItemsAttrs.containsBlank = '1';
3848
- }
3849
- // Check if all values are integers
3850
- if (field.minValue !== undefined && field.maxValue !== undefined) {
3851
- const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
3852
- if (isInteger) {
3853
- sharedItemsAttrs.containsInteger = '1';
3854
- }
3855
- sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
3856
- sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
3857
- }
3858
- } else if (field.hasBoolean) {
3859
- // Boolean-only field (no strings, no numbers)
3860
- if (field.hasBlank) {
3861
- sharedItemsAttrs.containsBlank = '1';
3862
- }
3863
- sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
3864
- sharedItemChildren.push(createElement('b', {
3865
- v: '0'
3866
- }, []));
3867
- sharedItemChildren.push(createElement('b', {
3868
- v: '1'
3869
- }, []));
3870
- if (field.hasBlank) {
3871
- sharedItemChildren.push(createElement('m', {}, []));
3872
- }
3873
- } else if (field.hasBlank) {
3874
- // Field that only contains blanks
3875
- sharedItemsAttrs.containsBlank = '1';
3876
- }
3877
- const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
3878
- const cacheFieldAttrs = {
3879
- name: field.name,
3880
- numFmtId: String(field.numFmtId ?? 0)
3881
- };
3882
- return createElement('cacheField', cacheFieldAttrs, [
3883
- sharedItemsNode
3884
- ]);
3885
- });
3886
- const cacheFieldsNode = createElement('cacheFields', {
3887
- count: String(this._fields.length)
3888
- }, cacheFieldNodes);
3889
- const worksheetSourceNode = createElement('worksheetSource', {
3890
- ref: this._sourceRange,
3891
- sheet: this._sourceSheet
3892
- }, []);
3893
- const cacheSourceNode = createElement('cacheSource', {
3894
- type: 'worksheet'
3895
- }, [
3896
- worksheetSourceNode
3897
- ]);
3898
- // Build attributes - align with Excel expectations
3899
- const definitionAttrs = {
3900
- xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3901
- 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3902
- 'r:id': recordsRelId
3903
- };
3904
- if (this._refreshOnLoad) {
3905
- definitionAttrs.refreshOnLoad = '1';
3906
- }
3907
- definitionAttrs.refreshedBy = 'User';
3908
- definitionAttrs.refreshedVersion = '8';
3909
- definitionAttrs.minRefreshableVersion = '3';
3910
- definitionAttrs.createdVersion = '8';
3911
- if (!this._saveData) {
3912
- definitionAttrs.saveData = '0';
3913
- definitionAttrs.recordCount = '0';
3914
- } else {
3915
- definitionAttrs.recordCount = String(this._recordCount);
3915
+ delete(path) {
3916
+ this._files.delete(path);
3917
+ this._deleted.add(path);
3918
+ if (this._entryNames) {
3919
+ this._entryNames.delete(path);
3916
3920
  }
3917
- const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
3918
- cacheSourceNode,
3919
- cacheFieldsNode
3920
- ]);
3921
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3922
- definitionNode
3923
- ])}`;
3924
3921
  }
3925
- /**
3926
- * Generate the pivotCacheRecords XML
3927
- */ toRecordsXml() {
3928
- const recordNodes = [];
3929
- for (const row of this._records){
3930
- const fieldNodes = [];
3931
- for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
3932
- const value = colIdx < row.length ? row[colIdx] : null;
3933
- if (value === null || value === undefined) {
3934
- // Missing value
3935
- const blankIndex = this._blankItemIndexMap.get(colIdx);
3936
- if (blankIndex !== undefined) {
3937
- fieldNodes.push(createElement('x', {
3938
- v: String(blankIndex)
3939
- }, []));
3940
- } else {
3941
- fieldNodes.push(createElement('m', {}, []));
3942
- }
3943
- } else if (typeof value === 'string') {
3944
- // String value - use index into sharedItems via O(1) Map lookup
3945
- const indexMap = this._sharedItemsIndexMap.get(colIdx);
3946
- const idx = indexMap?.get(value);
3947
- if (idx !== undefined) {
3948
- fieldNodes.push(createElement('x', {
3949
- v: String(idx)
3950
- }, []));
3951
- } else {
3952
- // Direct string value (shouldn't happen if cache is built correctly)
3953
- fieldNodes.push(createElement('s', {
3954
- v: value
3955
- }, []));
3956
- }
3957
- } else if (typeof value === 'number') {
3958
- if (this._fields[colIdx]?.isDate) {
3959
- const d = this._excelSerialToDate(value);
3960
- fieldNodes.push(createElement('d', {
3961
- v: this._formatDate(d)
3962
- }, []));
3963
- } else {
3964
- fieldNodes.push(createElement('n', {
3965
- v: String(value)
3966
- }, []));
3967
- }
3968
- } else if (typeof value === 'boolean') {
3969
- fieldNodes.push(createElement('b', {
3970
- v: value ? '1' : '0'
3971
- }, []));
3972
- } else if (value instanceof Date) {
3973
- fieldNodes.push(createElement('d', {
3974
- v: this._formatDate(value)
3975
- }, []));
3976
- } else {
3977
- // Unknown type, treat as missing
3978
- fieldNodes.push(createElement('m', {}, []));
3979
- }
3980
- }
3981
- recordNodes.push(createElement('r', {}, fieldNodes));
3982
- }
3983
- const recordsNode = createElement('pivotCacheRecords', {
3984
- xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3985
- 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3986
- count: String(this._recordCount)
3987
- }, recordNodes);
3988
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3989
- recordsNode
3990
- ])}`;
3922
+ getText(path) {
3923
+ const data = this.get(path);
3924
+ if (!data) return undefined;
3925
+ return fflate.strFromU8(data);
3991
3926
  }
3992
- _formatDate(value) {
3993
- return value.toISOString().replace(/\.\d{3}Z$/, '');
3927
+ setText(path, content) {
3928
+ this.set(path, fflate.strToU8(content));
3994
3929
  }
3995
- _formatNumber(value) {
3996
- if (Number.isInteger(value)) {
3997
- return String(value);
3930
+ async toFiles() {
3931
+ const unzipped = fflate.unzipSync(this._data);
3932
+ const files = new Map(Object.entries(unzipped));
3933
+ for (const path of this._deleted){
3934
+ files.delete(path);
3998
3935
  }
3999
- if (Math.abs(value) >= 1000000) {
4000
- return value.toFixed(16).replace(/0+$/, '').replace(/\.$/, '');
3936
+ for (const [path, content] of this._files){
3937
+ files.set(path, content);
4001
3938
  }
4002
- return String(value);
3939
+ return files;
4003
3940
  }
4004
- _excelSerialToDate(serial) {
4005
- // Excel epoch: December 31, 1899
4006
- const EXCEL_EPOCH = Date.UTC(1899, 11, 31);
4007
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
4008
- const adjusted = serial >= 60 ? serial - 1 : serial;
4009
- const ms = Math.round(adjusted * MS_PER_DAY);
4010
- return new Date(EXCEL_EPOCH + ms);
3941
+ _ensureIndex() {
3942
+ if (this._entryNames) return;
3943
+ const names = new Set();
3944
+ fflate.unzipSync(this._data, {
3945
+ filter: (file)=>{
3946
+ names.add(file.name);
3947
+ return false;
3948
+ }
3949
+ });
3950
+ this._entryNames = names;
4011
3951
  }
4012
3952
  }
4013
-
3953
+ const createZipStore = ()=>{
3954
+ return new EagerZipStore(new Map());
3955
+ };
4014
3956
  /**
4015
3957
  * Reads a ZIP file and returns a map of path -> content
4016
3958
  * @param data - ZIP file as Uint8Array
4017
3959
  * @returns Promise resolving to a map of file paths to contents
4018
- */ const readZip = (data)=>{
3960
+ */ const readZip = (data, options)=>{
3961
+ const lazy = options?.lazy ?? false;
3962
+ if (lazy) {
3963
+ return Promise.resolve(new LazyZipStore(data));
3964
+ }
4019
3965
  const isBun = typeof globalThis.Bun !== 'undefined';
4020
3966
  if (isBun) {
4021
3967
  try {
@@ -4024,7 +3970,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4024
3970
  for (const [path, content] of Object.entries(result)){
4025
3971
  files.set(path, content);
4026
3972
  }
4027
- return Promise.resolve(files);
3973
+ return Promise.resolve(new EagerZipStore(files));
4028
3974
  } catch (error) {
4029
3975
  return Promise.reject(error);
4030
3976
  }
@@ -4039,7 +3985,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4039
3985
  for (const [path, content] of Object.entries(result)){
4040
3986
  files.set(path, content);
4041
3987
  }
4042
- resolve(files);
3988
+ resolve(new EagerZipStore(files));
4043
3989
  });
4044
3990
  });
4045
3991
  };
@@ -4047,9 +3993,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4047
3993
  * Creates a ZIP file from a map of path -> content
4048
3994
  * @param files - Map of file paths to contents
4049
3995
  * @returns Promise resolving to ZIP file as Uint8Array
4050
- */ const writeZip = (files)=>{
3996
+ */ const writeZip = async (files)=>{
3997
+ const resolved = await files.toFiles();
4051
3998
  const zipData = {};
4052
- for (const [path, content] of files){
3999
+ for (const [path, content] of resolved){
4053
4000
  zipData[path] = content;
4054
4001
  }
4055
4002
  const isBun = typeof globalThis.Bun !== 'undefined';
@@ -4073,49 +4020,53 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4073
4020
  /**
4074
4021
  * Reads a file from the ZIP as a UTF-8 string
4075
4022
  */ const readZipText = (files, path)=>{
4076
- const data = files.get(path);
4077
- if (!data) return undefined;
4078
- return fflate.strFromU8(data);
4023
+ return files.getText(path);
4079
4024
  };
4080
4025
  /**
4081
4026
  * Writes a UTF-8 string to the ZIP files map
4082
4027
  */ const writeZipText = (files, path, content)=>{
4083
- files.set(path, fflate.strToU8(content));
4028
+ files.setText(path, content);
4084
4029
  };
4085
4030
 
4086
4031
  /**
4087
4032
  * Represents an Excel workbook (.xlsx file)
4088
4033
  */ class Workbook {
4089
4034
  constructor(){
4090
- this._files = new Map();
4035
+ this._files = createZipStore();
4091
4036
  this._sheets = new Map();
4092
4037
  this._sheetDefs = [];
4093
4038
  this._relationships = [];
4039
+ this._sharedStrings = null;
4040
+ this._styles = null;
4041
+ this._sharedStringsXml = null;
4042
+ this._stylesXml = null;
4043
+ this._lazy = true;
4094
4044
  this._dirty = false;
4095
- // Pivot table support
4096
- this._pivotTables = [];
4097
- this._pivotCaches = [];
4098
- this._nextCacheId = 5;
4099
- this._nextCacheFileIndex = 1;
4100
4045
  // Table support
4101
4046
  this._nextTableId = 1;
4047
+ // Pivot table support
4048
+ this._pivotTables = [];
4049
+ this._nextPivotTableId = 1;
4050
+ this._nextPivotCacheId = 1;
4102
4051
  // Date serialization handling
4103
4052
  this._dateHandling = 'jsDate';
4104
4053
  this._locale = 'fr-FR';
4105
- this._sharedStrings = new SharedStrings();
4106
- this._styles = Styles.createDefault();
4054
+ // Lazy init
4107
4055
  }
4108
4056
  /**
4109
4057
  * Load a workbook from a file path
4110
- */ static async fromFile(path) {
4058
+ */ static async fromFile(path, options = {}) {
4111
4059
  const data = await promises.readFile(path);
4112
- return Workbook.fromBuffer(new Uint8Array(data));
4060
+ return Workbook.fromBuffer(new Uint8Array(data), options);
4113
4061
  }
4114
4062
  /**
4115
4063
  * Load a workbook from a buffer
4116
- */ static async fromBuffer(data) {
4064
+ */ static async fromBuffer(data, options = {}) {
4117
4065
  const workbook = new Workbook();
4118
- workbook._files = await readZip(data);
4066
+ workbook._lazy = options.lazy ?? true;
4067
+ workbook._files = await readZip(data, {
4068
+ lazy: workbook._lazy
4069
+ });
4119
4070
  // Parse workbook.xml for sheet definitions
4120
4071
  const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
4121
4072
  if (workbookXml) {
@@ -4127,15 +4078,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4127
4078
  workbook._parseRelationships(relsXml);
4128
4079
  }
4129
4080
  // Parse shared strings
4130
- const sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml');
4131
- if (sharedStringsXml) {
4132
- workbook._sharedStrings = SharedStrings.parse(sharedStringsXml);
4133
- }
4134
- // Parse styles
4135
- const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
4136
- if (stylesXml) {
4137
- workbook._styles = Styles.parse(stylesXml);
4138
- }
4081
+ // Store shared strings/styles XML for lazy parse
4082
+ workbook._sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml') ?? null;
4083
+ workbook._stylesXml = readZipText(workbook._files, 'xl/styles.xml') ?? null;
4139
4084
  return workbook;
4140
4085
  }
4141
4086
  /**
@@ -4143,6 +4088,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4143
4088
  */ static create() {
4144
4089
  const workbook = new Workbook();
4145
4090
  workbook._dirty = true;
4091
+ workbook._lazy = false;
4092
+ workbook._sharedStrings = new SharedStrings();
4093
+ workbook._styles = Styles.createDefault();
4146
4094
  return workbook;
4147
4095
  }
4148
4096
  /**
@@ -4158,11 +4106,25 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4158
4106
  /**
4159
4107
  * Get shared strings table
4160
4108
  */ get sharedStrings() {
4109
+ if (!this._sharedStrings) {
4110
+ if (this._sharedStringsXml) {
4111
+ this._sharedStrings = SharedStrings.parse(this._sharedStringsXml);
4112
+ } else {
4113
+ this._sharedStrings = new SharedStrings();
4114
+ }
4115
+ }
4161
4116
  return this._sharedStrings;
4162
4117
  }
4163
4118
  /**
4164
4119
  * Get styles
4165
4120
  */ get styles() {
4121
+ if (!this._styles) {
4122
+ if (this._stylesXml) {
4123
+ this._styles = Styles.parse(this._stylesXml);
4124
+ } else {
4125
+ this._styles = Styles.createDefault();
4126
+ }
4127
+ }
4166
4128
  return this._styles;
4167
4129
  }
4168
4130
  /**
@@ -4193,6 +4155,72 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4193
4155
  return this._nextTableId++;
4194
4156
  }
4195
4157
  /**
4158
+ * Get all pivot tables in the workbook.
4159
+ */ get pivotTables() {
4160
+ return [
4161
+ ...this._pivotTables
4162
+ ];
4163
+ }
4164
+ /**
4165
+ * Create a new pivot table.
4166
+ */ createPivotTable(config) {
4167
+ if (!config.name || config.name.trim().length === 0) {
4168
+ throw new Error('Pivot table name is required');
4169
+ }
4170
+ if (this._pivotTables.some((pivot)=>pivot.name === config.name)) {
4171
+ throw new Error(`Pivot table name already exists: ${config.name}`);
4172
+ }
4173
+ const sourceRef = parseSheetRange(config.source);
4174
+ const targetRef = parseSheetAddress(config.target);
4175
+ const sourceSheet = this.sheet(sourceRef.sheet);
4176
+ this.sheet(targetRef.sheet);
4177
+ const sourceRange = this._normalizeRange(sourceRef.range);
4178
+ if (sourceRange.start.row >= sourceRange.end.row) {
4179
+ throw new Error('Pivot source range must include a header row and at least one data row');
4180
+ }
4181
+ const fields = this._extractPivotFields(sourceSheet, sourceRange);
4182
+ const cacheId = this._nextPivotCacheId++;
4183
+ const pivotId = this._nextPivotTableId++;
4184
+ const cachePartIndex = this._pivotTables.length + 1;
4185
+ const pivot = new PivotTable(this, config, sourceRef.sheet, sourceSheet, sourceRange, targetRef.sheet, targetRef.address, cacheId, pivotId, cachePartIndex, fields);
4186
+ this._pivotTables.push(pivot);
4187
+ this._dirty = true;
4188
+ return pivot;
4189
+ }
4190
+ _extractPivotFields(sourceSheet, sourceRange) {
4191
+ const fields = [];
4192
+ const seen = new Set();
4193
+ for(let col = sourceRange.start.col; col <= sourceRange.end.col; col++){
4194
+ const headerCell = sourceSheet.getCellIfExists(sourceRange.start.row, col);
4195
+ const rawHeader = headerCell?.value;
4196
+ const name = rawHeader == null ? `Column${col - sourceRange.start.col + 1}` : String(rawHeader).trim();
4197
+ if (!name) {
4198
+ throw new Error(`Pivot source header is empty at column ${col + 1}`);
4199
+ }
4200
+ if (seen.has(name)) {
4201
+ throw new Error(`Duplicate pivot source header: ${name}`);
4202
+ }
4203
+ seen.add(name);
4204
+ fields.push({
4205
+ name,
4206
+ sourceCol: col
4207
+ });
4208
+ }
4209
+ return fields;
4210
+ }
4211
+ _normalizeRange(range) {
4212
+ return {
4213
+ start: {
4214
+ row: Math.min(range.start.row, range.end.row),
4215
+ col: Math.min(range.start.col, range.end.col)
4216
+ },
4217
+ end: {
4218
+ row: Math.max(range.start.row, range.end.row),
4219
+ col: Math.max(range.start.col, range.end.col)
4220
+ }
4221
+ };
4222
+ }
4223
+ /**
4196
4224
  * Get a worksheet by name or index
4197
4225
  */ sheet(nameOrIndex) {
4198
4226
  let def;
@@ -4216,7 +4244,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4216
4244
  const sheetPath = `xl/${rel.target}`;
4217
4245
  const sheetXml = readZipText(this._files, sheetPath);
4218
4246
  if (sheetXml) {
4219
- worksheet.parse(sheetXml);
4247
+ worksheet.parse(sheetXml, {
4248
+ lazy: this._lazy
4249
+ });
4220
4250
  }
4221
4251
  }
4222
4252
  this._sheets.set(def.name, worksheet);
@@ -4527,100 +4557,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4527
4557
  return String(value);
4528
4558
  }
4529
4559
  /**
4530
- * Create a pivot table from source data.
4531
- *
4532
- * @param config - Pivot table configuration
4533
- * @returns PivotTable instance for fluent configuration
4534
- *
4535
- * @example
4536
- * ```typescript
4537
- * const pivot = wb.createPivotTable({
4538
- * name: 'SalesPivot',
4539
- * source: 'DataSheet!A1:D100',
4540
- * target: 'PivotSheet!A3',
4541
- * });
4542
- *
4543
- * pivot
4544
- * .addRowField('Region')
4545
- * .addColumnField('Product')
4546
- * .addValueField('Sales', 'sum', 'Total Sales');
4547
- * ```
4548
- */ createPivotTable(config) {
4549
- this._dirty = true;
4550
- // Parse source reference (Sheet!Range)
4551
- const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
4552
- // Parse target reference
4553
- const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
4554
- // Ensure target sheet exists
4555
- if (!this._sheetDefs.some((s)=>s.name === targetSheet)) {
4556
- this.addSheet(targetSheet);
4557
- }
4558
- // Parse target cell address
4559
- const targetAddr = parseAddress(targetCell);
4560
- // Get source worksheet and extract data
4561
- const sourceWs = this.sheet(sourceSheet);
4562
- const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
4563
- // Create pivot cache
4564
- const cacheId = this._nextCacheId++;
4565
- const cacheFileIndex = this._nextCacheFileIndex++;
4566
- const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
4567
- cache.setStyles(this._styles);
4568
- cache.buildFromData(headers, data);
4569
- // refreshOnLoad defaults to true; only disable if explicitly set to false
4570
- if (config.refreshOnLoad === false) {
4571
- cache.refreshOnLoad = false;
4572
- }
4573
- // saveData defaults to true; only disable if explicitly set to false
4574
- if (config.saveData === false) {
4575
- cache.saveData = false;
4576
- }
4577
- this._pivotCaches.push(cache);
4578
- // Create pivot table
4579
- const pivotTableIndex = this._pivotTables.length + 1;
4580
- const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex, cacheFileIndex);
4581
- // Set styles reference for number format resolution
4582
- pivotTable.setStyles(this._styles);
4583
- this._pivotTables.push(pivotTable);
4584
- return pivotTable;
4585
- }
4586
- /**
4587
- * Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
4588
- */ _parseSheetRef(ref) {
4589
- const match = ref.match(/^(.+?)!(.+)$/);
4590
- if (!match) {
4591
- throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
4592
- }
4593
- return {
4594
- sheetName: match[1],
4595
- range: match[2]
4596
- };
4597
- }
4598
- /**
4599
- * Extract headers and data from a source range
4600
- */ _extractSourceData(sheet, rangeStr) {
4601
- const range = parseRange(rangeStr);
4602
- const headers = [];
4603
- const data = [];
4604
- // First row is headers
4605
- for(let col = range.start.col; col <= range.end.col; col++){
4606
- const cell = sheet.cell(toAddress(range.start.row, col));
4607
- headers.push(String(cell.value ?? `Column${col + 1}`));
4608
- }
4609
- // Remaining rows are data
4610
- for(let row = range.start.row + 1; row <= range.end.row; row++){
4611
- const rowData = [];
4612
- for(let col = range.start.col; col <= range.end.col; col++){
4613
- const cell = sheet.cell(toAddress(row, col));
4614
- rowData.push(cell.value);
4615
- }
4616
- data.push(rowData);
4617
- }
4618
- return {
4619
- headers,
4620
- data
4621
- };
4622
- }
4623
- /**
4624
4560
  * Save the workbook to a file
4625
4561
  */ async toFile(path) {
4626
4562
  const buffer = await this.toBuffer();
@@ -4678,20 +4614,28 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4678
4614
  _updateFiles() {
4679
4615
  const relationshipInfo = this._buildRelationshipInfo();
4680
4616
  // Update workbook.xml
4681
- this._updateWorkbookXml(relationshipInfo.pivotCacheRelIds);
4617
+ this._updateWorkbookXml(relationshipInfo.pivotCacheRelByTarget);
4682
4618
  // Update relationships
4683
4619
  this._updateRelationshipsXml(relationshipInfo.relNodes);
4684
4620
  // Update content types
4685
4621
  this._updateContentTypes();
4686
4622
  // Update shared strings if modified
4687
- if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
4688
- writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());
4623
+ if (this._sharedStrings) {
4624
+ if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
4625
+ writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());
4626
+ }
4627
+ } else if (this._sharedStringsXml) {
4628
+ writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStringsXml);
4689
4629
  }
4690
4630
  // Update styles if modified or if file doesn't exist yet
4691
- if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
4692
- writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
4631
+ if (this._styles) {
4632
+ if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
4633
+ writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
4634
+ }
4635
+ } else if (this._stylesXml) {
4636
+ writeZipText(this._files, 'xl/styles.xml', this._stylesXml);
4693
4637
  }
4694
- // Update worksheets (needed for pivot table targets)
4638
+ // Update worksheets
4695
4639
  for (const [name, worksheet] of this._sheets){
4696
4640
  if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
4697
4641
  const def = this._sheetDefs.find((s)=>s.name === name);
@@ -4704,15 +4648,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4704
4648
  }
4705
4649
  }
4706
4650
  }
4707
- // Update pivot tables
4708
- if (this._pivotTables.length > 0) {
4709
- this._updatePivotTableFiles();
4710
- }
4711
4651
  // Update tables (sets table rel IDs for tableParts)
4712
4652
  this._updateTableFiles();
4653
+ // Update pivot tables (sets pivot rel IDs for pivotTableParts)
4654
+ this._updatePivotFiles();
4713
4655
  // Update worksheets to align tableParts with relationship IDs
4714
4656
  for (const [name, worksheet] of this._sheets){
4715
- if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
4657
+ if (worksheet.dirty || this._dirty || worksheet.tables.length > 0 || this._pivotTables.length > 0) {
4716
4658
  const def = this._sheetDefs.find((s)=>s.name === name);
4717
4659
  if (def) {
4718
4660
  const rel = this._relationships.find((r)=>r.id === def.rId);
@@ -4724,7 +4666,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4724
4666
  }
4725
4667
  }
4726
4668
  }
4727
- _updateWorkbookXml(pivotCacheRelIds) {
4669
+ _updateWorkbookXml(pivotCacheRelByTarget) {
4728
4670
  const sheetNodes = this._sheetDefs.map((def)=>createElement('sheet', {
4729
4671
  name: def.name,
4730
4672
  sheetId: String(def.sheetId),
@@ -4734,19 +4676,20 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4734
4676
  const children = [
4735
4677
  sheetsNode
4736
4678
  ];
4737
- // Add pivot caches if any
4738
- if (this._pivotCaches.length > 0) {
4739
- const pivotCacheNodes = this._pivotCaches.map((cache)=>{
4740
- const cacheRelId = pivotCacheRelIds.get(cache.cacheId);
4741
- if (!cacheRelId) {
4742
- throw new Error(`Missing pivot cache relationship ID for cache ${cache.cacheId}`);
4743
- }
4744
- return createElement('pivotCache', {
4745
- cacheId: String(cache.cacheId),
4746
- 'r:id': cacheRelId
4747
- }, []);
4748
- });
4749
- children.push(createElement('pivotCaches', {}, pivotCacheNodes));
4679
+ if (this._pivotTables.length > 0) {
4680
+ const pivotCacheNodes = [];
4681
+ for (const pivot of this._pivotTables){
4682
+ const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
4683
+ const relId = pivotCacheRelByTarget.get(target);
4684
+ if (!relId) continue;
4685
+ pivotCacheNodes.push(createElement('pivotCache', {
4686
+ cacheId: String(pivot.cacheId),
4687
+ 'r:id': relId
4688
+ }, []));
4689
+ }
4690
+ if (pivotCacheNodes.length > 0) {
4691
+ children.push(createElement('pivotCaches', {}, pivotCacheNodes));
4692
+ }
4750
4693
  }
4751
4694
  const workbookNode = createElement('workbook', {
4752
4695
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
@@ -4772,6 +4715,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4772
4715
  Type: rel.type,
4773
4716
  Target: rel.target
4774
4717
  }, []));
4718
+ const pivotCacheRelByTarget = new Map();
4775
4719
  const reservedRelIds = new Set(relNodes.map((node)=>getAttr(node, 'Id') || '').filter(Boolean));
4776
4720
  let nextRelId = Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
4777
4721
  const allocateRelId = ()=>{
@@ -4784,7 +4728,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4784
4728
  return id;
4785
4729
  };
4786
4730
  // Add shared strings relationship if needed
4787
- if (this._sharedStrings.count > 0) {
4731
+ const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
4732
+ if (shouldIncludeSharedStrings) {
4788
4733
  const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
4789
4734
  if (!hasSharedStrings) {
4790
4735
  relNodes.push(createElement('Relationship', {
@@ -4803,23 +4748,32 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4803
4748
  Target: 'styles.xml'
4804
4749
  }, []));
4805
4750
  }
4806
- // Add pivot cache relationships
4807
- const pivotCacheRelIds = new Map();
4808
- for (const cache of this._pivotCaches){
4809
- const id = allocateRelId();
4810
- pivotCacheRelIds.set(cache.cacheId, id);
4811
- relNodes.push(createElement('Relationship', {
4812
- Id: id,
4813
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
4814
- Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`
4815
- }, []));
4751
+ for (const pivot of this._pivotTables){
4752
+ const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
4753
+ const hasPivotCacheRel = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' && getAttr(node, 'Target') === target);
4754
+ if (!hasPivotCacheRel) {
4755
+ const id = allocateRelId();
4756
+ pivotCacheRelByTarget.set(target, id);
4757
+ relNodes.push(createElement('Relationship', {
4758
+ Id: id,
4759
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
4760
+ Target: target
4761
+ }, []));
4762
+ } else {
4763
+ const existing = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' && getAttr(node, 'Target') === target);
4764
+ const existingId = existing ? getAttr(existing, 'Id') : undefined;
4765
+ if (existingId) {
4766
+ pivotCacheRelByTarget.set(target, existingId);
4767
+ }
4768
+ }
4816
4769
  }
4817
4770
  return {
4818
4771
  relNodes,
4819
- pivotCacheRelIds
4772
+ pivotCacheRelByTarget
4820
4773
  };
4821
4774
  }
4822
4775
  _updateContentTypes() {
4776
+ const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
4823
4777
  const types = [
4824
4778
  createElement('Default', {
4825
4779
  Extension: 'rels',
@@ -4839,7 +4793,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4839
4793
  }, [])
4840
4794
  ];
4841
4795
  // Add shared strings if present
4842
- if (this._sharedStrings.count > 0) {
4796
+ if (shouldIncludeSharedStrings) {
4843
4797
  types.push(createElement('Override', {
4844
4798
  PartName: '/xl/sharedStrings.xml',
4845
4799
  ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'
@@ -4855,24 +4809,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4855
4809
  }, []));
4856
4810
  }
4857
4811
  }
4858
- // Add pivot cache definitions and records
4859
- for (const cache of this._pivotCaches){
4860
- types.push(createElement('Override', {
4861
- PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
4862
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
4863
- }, []));
4864
- types.push(createElement('Override', {
4865
- PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,
4866
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
4867
- }, []));
4868
- }
4869
- // Add pivot tables
4870
- for (const pivotTable of this._pivotTables){
4871
- types.push(createElement('Override', {
4872
- PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,
4873
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
4874
- }, []));
4875
- }
4876
4812
  // Add tables
4877
4813
  let tableIndex = 1;
4878
4814
  for (const def of this._sheetDefs){
@@ -4887,6 +4823,21 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4887
4823
  }
4888
4824
  }
4889
4825
  }
4826
+ // Add pivot caches and pivot tables
4827
+ for (const pivot of this._pivotTables){
4828
+ types.push(createElement('Override', {
4829
+ PartName: `/xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`,
4830
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
4831
+ }, []));
4832
+ types.push(createElement('Override', {
4833
+ PartName: `/xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`,
4834
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
4835
+ }, []));
4836
+ types.push(createElement('Override', {
4837
+ PartName: `/xl/pivotTables/pivotTable${pivot.pivotId}.xml`,
4838
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
4839
+ }, []));
4840
+ }
4890
4841
  const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
4891
4842
  const existingKeys = new Set(types.map((t)=>{
4892
4843
  if ('Default' in t) {
@@ -4942,66 +4893,35 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4942
4893
  }
4943
4894
  }
4944
4895
  /**
4945
- * Generate all pivot table related files
4946
- */ _updatePivotTableFiles() {
4947
- // Track which sheets have pivot tables for their .rels files
4948
- const sheetPivotTables = new Map();
4949
- for (const pivotTable of this._pivotTables){
4950
- const sheetName = pivotTable.targetSheet;
4951
- if (!sheetPivotTables.has(sheetName)) {
4952
- sheetPivotTables.set(sheetName, []);
4896
+ * Generate all table related files
4897
+ */ _updateTableFiles() {
4898
+ // Collect all tables with their global indices
4899
+ let globalTableIndex = 1;
4900
+ const sheetTables = new Map();
4901
+ for (const def of this._sheetDefs){
4902
+ const worksheet = this._sheets.get(def.name);
4903
+ if (!worksheet) continue;
4904
+ const tables = worksheet.tables;
4905
+ if (tables.length === 0) continue;
4906
+ const tableInfos = [];
4907
+ for (const table of tables){
4908
+ tableInfos.push({
4909
+ table,
4910
+ globalIndex: globalTableIndex
4911
+ });
4912
+ globalTableIndex++;
4953
4913
  }
4954
- sheetPivotTables.get(sheetName).push(pivotTable);
4955
- }
4956
- // Generate pivot cache files
4957
- for(let i = 0; i < this._pivotCaches.length; i++){
4958
- const cache = this._pivotCaches[i];
4959
- // Pivot cache definition
4960
- const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;
4961
- writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
4962
- // Pivot cache records
4963
- const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;
4964
- writeZipText(this._files, recordsPath, cache.toRecordsXml());
4965
- // Pivot cache definition relationships (link to records)
4966
- const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;
4967
- const cacheRels = createElement('Relationships', {
4968
- xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
4969
- }, [
4970
- createElement('Relationship', {
4971
- Id: 'rId1',
4972
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
4973
- Target: `pivotCacheRecords${cache.fileIndex}.xml`
4974
- }, [])
4975
- ]);
4976
- writeZipText(this._files, cacheRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
4977
- cacheRels
4978
- ])}`);
4914
+ sheetTables.set(def.name, tableInfos);
4979
4915
  }
4980
- // Generate pivot table files
4981
- for(let i = 0; i < this._pivotTables.length; i++){
4982
- const pivotTable = this._pivotTables[i];
4983
- const ptIdx = pivotTable.index;
4984
- // Pivot table definition
4985
- const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
4986
- writeZipText(this._files, ptPath, pivotTable.toXml());
4987
- // Pivot table relationships (link to cache definition)
4988
- const cacheIdx = pivotTable.cacheFileIndex;
4989
- const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
4990
- const ptRels = createElement('Relationships', {
4991
- xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
4992
- }, [
4993
- createElement('Relationship', {
4994
- Id: 'rId1',
4995
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
4996
- Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`
4997
- }, [])
4998
- ]);
4999
- writeZipText(this._files, ptRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
5000
- ptRels
5001
- ])}`);
4916
+ // Generate table files
4917
+ for (const [, tableInfos] of sheetTables){
4918
+ for (const { table, globalIndex } of tableInfos){
4919
+ const tablePath = `xl/tables/table${globalIndex}.xml`;
4920
+ writeZipText(this._files, tablePath, table.toXml());
4921
+ }
5002
4922
  }
5003
- // Generate worksheet relationships for pivot tables
5004
- for (const [sheetName, pivotTables] of sheetPivotTables){
4923
+ // Generate worksheet relationships for tables
4924
+ for (const [sheetName, tableInfos] of sheetTables){
5005
4925
  const def = this._sheetDefs.find((s)=>s.name === sheetName);
5006
4926
  if (!def) continue;
5007
4927
  const rel = this._relationships.find((r)=>r.id === def.rId);
@@ -5009,11 +4929,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5009
4929
  // Extract sheet file name from target path
5010
4930
  const sheetFileName = rel.target.split('/').pop();
5011
4931
  const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
4932
+ // Check if there are already pivot table relationships for this sheet
5012
4933
  const existingRelsXml = readZipText(this._files, sheetRelsPath);
5013
- let relNodes = [];
5014
4934
  let nextRelId = 1;
4935
+ const relNodes = [];
5015
4936
  const reservedRelIds = new Set();
5016
4937
  if (existingRelsXml) {
4938
+ // Parse existing rels and find max rId
5017
4939
  const parsed = parseXml(existingRelsXml);
5018
4940
  const relsElement = findElement(parsed, 'Relationships');
5019
4941
  if (relsElement) {
@@ -5042,16 +4964,29 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5042
4964
  reservedRelIds.add(id);
5043
4965
  return id;
5044
4966
  };
5045
- for (const pt of pivotTables){
5046
- const target = `../pivotTables/pivotTable${pt.index}.xml`;
5047
- const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' && getAttr(node, 'Target') === target);
5048
- if (existing) continue;
4967
+ // Add table relationships
4968
+ const tableRelIds = [];
4969
+ for (const { globalIndex } of tableInfos){
4970
+ const target = `../tables/table${globalIndex}.xml`;
4971
+ const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
4972
+ if (existing) {
4973
+ const existingRel = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
4974
+ const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
4975
+ tableRelIds.push(existingId ?? allocateRelId());
4976
+ continue;
4977
+ }
4978
+ const id = allocateRelId();
4979
+ tableRelIds.push(id);
5049
4980
  relNodes.push(createElement('Relationship', {
5050
- Id: allocateRelId(),
5051
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
4981
+ Id: id,
4982
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
5052
4983
  Target: target
5053
4984
  }, []));
5054
4985
  }
4986
+ const worksheet = this._sheets.get(sheetName);
4987
+ if (worksheet) {
4988
+ worksheet.setTableRelIds(tableRelIds);
4989
+ }
5055
4990
  const sheetRels = createElement('Relationships', {
5056
4991
  xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
5057
4992
  }, relNodes);
@@ -5061,54 +4996,45 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5061
4996
  }
5062
4997
  }
5063
4998
  /**
5064
- * Generate all table related files
5065
- */ _updateTableFiles() {
5066
- // Collect all tables with their global indices
5067
- let globalTableIndex = 1;
5068
- const sheetTables = new Map();
5069
- for (const def of this._sheetDefs){
5070
- const worksheet = this._sheets.get(def.name);
5071
- if (!worksheet) continue;
5072
- const tables = worksheet.tables;
5073
- if (tables.length === 0) continue;
5074
- const tableInfos = [];
5075
- for (const table of tables){
5076
- tableInfos.push({
5077
- table,
5078
- globalIndex: globalTableIndex
5079
- });
5080
- globalTableIndex++;
5081
- }
5082
- sheetTables.set(def.name, tableInfos);
5083
- }
5084
- // Generate table files
5085
- for (const [, tableInfos] of sheetTables){
5086
- for (const { table, globalIndex } of tableInfos){
5087
- const tablePath = `xl/tables/table${globalIndex}.xml`;
5088
- writeZipText(this._files, tablePath, table.toXml());
5089
- }
4999
+ * Generate pivot cache/table parts and worksheet relationships.
5000
+ */ _updatePivotFiles() {
5001
+ if (this._pivotTables.length === 0) {
5002
+ return;
5090
5003
  }
5091
- // Generate worksheet relationships for tables
5092
- for (const [sheetName, tableInfos] of sheetTables){
5004
+ for (const pivot of this._pivotTables){
5005
+ const pivotParts = pivot.buildPivotPartsXml();
5006
+ const pivotCachePath = `xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
5007
+ writeZipText(this._files, pivotCachePath, pivotParts.cacheDefinitionXml);
5008
+ const pivotCacheRecordsPath = `xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`;
5009
+ writeZipText(this._files, pivotCacheRecordsPath, pivotParts.cacheRecordsXml);
5010
+ const pivotCacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${pivot.cachePartIndex}.xml.rels`;
5011
+ writeZipText(this._files, pivotCacheRelsPath, pivotParts.cacheRelsXml);
5012
+ const pivotTablePath = `xl/pivotTables/pivotTable${pivot.pivotId}.xml`;
5013
+ writeZipText(this._files, pivotTablePath, pivotParts.pivotTableXml);
5014
+ }
5015
+ const pivotsBySheet = new Map();
5016
+ for (const pivot of this._pivotTables){
5017
+ const existing = pivotsBySheet.get(pivot.targetSheetName) ?? [];
5018
+ existing.push(pivot);
5019
+ pivotsBySheet.set(pivot.targetSheetName, existing);
5020
+ }
5021
+ for (const [sheetName, pivots] of pivotsBySheet){
5093
5022
  const def = this._sheetDefs.find((s)=>s.name === sheetName);
5094
5023
  if (!def) continue;
5095
5024
  const rel = this._relationships.find((r)=>r.id === def.rId);
5096
5025
  if (!rel) continue;
5097
- // Extract sheet file name from target path
5098
5026
  const sheetFileName = rel.target.split('/').pop();
5027
+ if (!sheetFileName) continue;
5099
5028
  const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
5100
- // Check if there are already pivot table relationships for this sheet
5101
5029
  const existingRelsXml = readZipText(this._files, sheetRelsPath);
5102
5030
  let nextRelId = 1;
5103
5031
  const relNodes = [];
5104
5032
  const reservedRelIds = new Set();
5105
5033
  if (existingRelsXml) {
5106
- // Parse existing rels and find max rId
5107
5034
  const parsed = parseXml(existingRelsXml);
5108
5035
  const relsElement = findElement(parsed, 'Relationships');
5109
5036
  if (relsElement) {
5110
- const existingRelNodes = getChildren(relsElement, 'Relationships');
5111
- for (const relNode of existingRelNodes){
5037
+ for (const relNode of getChildren(relsElement, 'Relationships')){
5112
5038
  if ('Relationship' in relNode) {
5113
5039
  relNodes.push(relNode);
5114
5040
  const id = getAttr(relNode, 'Id');
@@ -5132,28 +5058,26 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5132
5058
  reservedRelIds.add(id);
5133
5059
  return id;
5134
5060
  };
5135
- // Add table relationships
5136
- const tableRelIds = [];
5137
- for (const { globalIndex } of tableInfos){
5138
- const target = `../tables/table${globalIndex}.xml`;
5139
- const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
5061
+ const pivotRelIds = [];
5062
+ for (const pivot of pivots){
5063
+ const target = `../pivotTables/pivotTable${pivot.pivotId}.xml`;
5064
+ const existing = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' && getAttr(node, 'Target') === target);
5140
5065
  if (existing) {
5141
- const existingRel = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
5142
- const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
5143
- tableRelIds.push(existingId ?? allocateRelId());
5066
+ const existingId = getAttr(existing, 'Id');
5067
+ pivotRelIds.push(existingId ?? allocateRelId());
5144
5068
  continue;
5145
5069
  }
5146
5070
  const id = allocateRelId();
5147
- tableRelIds.push(id);
5071
+ pivotRelIds.push(id);
5148
5072
  relNodes.push(createElement('Relationship', {
5149
5073
  Id: id,
5150
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
5074
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
5151
5075
  Target: target
5152
5076
  }, []));
5153
5077
  }
5154
5078
  const worksheet = this._sheets.get(sheetName);
5155
5079
  if (worksheet) {
5156
- worksheet.setTableRelIds(tableRelIds);
5080
+ worksheet.setPivotTableRelIds(pivotRelIds);
5157
5081
  }
5158
5082
  const sheetRels = createElement('Relationships', {
5159
5083
  xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
@@ -5166,7 +5090,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5166
5090
  }
5167
5091
 
5168
5092
  exports.Cell = Cell;
5169
- exports.PivotCache = PivotCache;
5170
5093
  exports.PivotTable = PivotTable;
5171
5094
  exports.Range = Range;
5172
5095
  exports.SharedStrings = SharedStrings;
@@ -5176,5 +5099,7 @@ exports.Workbook = Workbook;
5176
5099
  exports.Worksheet = Worksheet;
5177
5100
  exports.parseAddress = parseAddress;
5178
5101
  exports.parseRange = parseRange;
5102
+ exports.parseSheetAddress = parseSheetAddress;
5103
+ exports.parseSheetRange = parseSheetRange;
5179
5104
  exports.toAddress = toAddress;
5180
5105
  exports.toRange = toRange;