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