@niicojs/excel 0.3.3 → 0.3.5

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