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