@niicojs/excel 0.3.4 → 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,672 +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
+ ]);
3620
+ }
3621
+ if (!isAxisField) {
3622
+ return createElement('cacheField', {
3623
+ name: field.name,
3624
+ numFmtId: '0'
3625
+ }, [
3626
+ createElement('sharedItems', {}, [])
3627
+ ]);
3435
3628
  }
3436
- return createElement('pivotField', attrs, children);
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;
3847
+ class EagerZipStore {
3848
+ constructor(files){
3849
+ this._files = files;
3626
3850
  }
3627
- /**
3628
- * Set styles reference for number format resolution.
3629
- * @internal
3630
- */ setStyles(styles) {
3631
- this._styles = styles;
3851
+ get(path) {
3852
+ return this._files.get(path);
3632
3853
  }
3633
- /**
3634
- * Get the cache ID
3635
- */ get cacheId() {
3636
- return this._cacheId;
3854
+ set(path, content) {
3855
+ this._files.set(path, content);
3637
3856
  }
3638
- /**
3639
- * Get the file index for this cache (used for file naming).
3640
- */ get fileIndex() {
3641
- return this._fileIndex;
3857
+ has(path) {
3858
+ return this._files.has(path);
3642
3859
  }
3643
- /**
3644
- * Set refreshOnLoad option
3645
- */ set refreshOnLoad(value) {
3646
- this._refreshOnLoad = value;
3860
+ delete(path) {
3861
+ this._files.delete(path);
3647
3862
  }
3648
- /**
3649
- * Set saveData option
3650
- */ set saveData(value) {
3651
- this._saveData = value;
3863
+ getText(path) {
3864
+ const data = this._files.get(path);
3865
+ if (!data) return undefined;
3866
+ return strFromU8(data);
3652
3867
  }
3653
- /**
3654
- * Get refreshOnLoad option
3655
- */ get refreshOnLoad() {
3656
- return this._refreshOnLoad;
3868
+ setText(path, content) {
3869
+ this._files.set(path, strToU8(content));
3657
3870
  }
3658
- /**
3659
- * Get saveData option
3660
- */ get saveData() {
3661
- return this._saveData;
3662
- }
3663
- /**
3664
- * Get the source sheet name
3665
- */ get sourceSheet() {
3666
- return this._sourceSheet;
3667
- }
3668
- /**
3669
- * Get the source range
3670
- */ get sourceRange() {
3671
- return this._sourceRange;
3672
- }
3673
- /**
3674
- * Get the full source reference (Sheet!Range)
3675
- */ get sourceRef() {
3676
- return `${this._sourceSheet}!${this._sourceRange}`;
3677
- }
3678
- /**
3679
- * Get the fields in this cache
3680
- */ get fields() {
3681
- return this._fields;
3871
+ toFiles() {
3872
+ return Promise.resolve(this._files);
3682
3873
  }
3683
- /**
3684
- * Get the number of data records
3685
- */ get recordCount() {
3686
- return this._recordCount;
3687
- }
3688
- /**
3689
- * Build the cache from source data.
3690
- * @param headers - Array of column header names
3691
- * @param data - 2D array of data rows (excluding headers)
3692
- */ buildFromData(headers, data) {
3693
- this._recordCount = data.length;
3694
- // Initialize fields from headers
3695
- this._fields = headers.map((name, index)=>({
3696
- name,
3697
- index,
3698
- isNumeric: true,
3699
- isDate: false,
3700
- hasBoolean: false,
3701
- hasBlank: false,
3702
- numFmtId: undefined,
3703
- sharedItems: [],
3704
- minValue: undefined,
3705
- maxValue: undefined,
3706
- minDate: undefined,
3707
- maxDate: undefined
3708
- }));
3709
- // Use Maps for unique value collection during analysis
3710
- const sharedItemsMaps = this._fields.map(()=>new Map());
3711
- // Analyze data to determine field types and collect unique values
3712
- for (const row of data){
3713
- for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
3714
- const value = row[colIdx];
3715
- const field = this._fields[colIdx];
3716
- if (value === null || value === undefined) {
3717
- field.hasBlank = true;
3718
- continue;
3719
- }
3720
- if (typeof value === 'string') {
3721
- field.isNumeric = false;
3722
- const map = sharedItemsMaps[colIdx];
3723
- if (!map.has(value)) {
3724
- map.set(value, value);
3725
- }
3726
- } else if (typeof value === 'number') {
3727
- if (field.isDate) {
3728
- const d = this._excelSerialToDate(value);
3729
- if (!field.minDate || d < field.minDate) {
3730
- field.minDate = d;
3731
- }
3732
- if (!field.maxDate || d > field.maxDate) {
3733
- field.maxDate = d;
3734
- }
3735
- } else {
3736
- if (field.minValue === undefined || value < field.minValue) {
3737
- field.minValue = value;
3738
- }
3739
- if (field.maxValue === undefined || value > field.maxValue) {
3740
- field.maxValue = value;
3741
- }
3742
- }
3743
- } else if (value instanceof Date) {
3744
- field.isDate = true;
3745
- field.isNumeric = false;
3746
- if (!field.minDate || value < field.minDate) {
3747
- field.minDate = value;
3748
- }
3749
- if (!field.maxDate || value > field.maxDate) {
3750
- field.maxDate = value;
3751
- }
3752
- } else if (typeof value === 'boolean') {
3753
- field.isNumeric = false;
3754
- field.hasBoolean = true;
3755
- }
3756
- }
3757
- }
3758
- // Resolve number formats if styles are available
3759
- if (this._styles) {
3760
- const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
3761
- for (const field of this._fields){
3762
- if (field.isDate) {
3763
- field.numFmtId = dateFmtId;
3764
- }
3765
- }
3766
- }
3767
- // Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
3768
- this._sharedItemsIndexMap.clear();
3769
- this._blankItemIndexMap.clear();
3770
- for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
3771
- const field = this._fields[colIdx];
3772
- const map = sharedItemsMaps[colIdx];
3773
- // Convert Map values to array (maintains insertion order in ES6+)
3774
- field.sharedItems = Array.from(map.values());
3775
- // Build reverse lookup Map: value -> index
3776
- if (field.sharedItems.length > 0) {
3777
- const indexMap = new Map();
3778
- for(let i = 0; i < field.sharedItems.length; i++){
3779
- indexMap.set(field.sharedItems[i], i);
3780
- }
3781
- this._sharedItemsIndexMap.set(colIdx, indexMap);
3782
- if (field.hasBlank) {
3783
- const blankIndex = field.sharedItems.length;
3784
- this._blankItemIndexMap.set(colIdx, blankIndex);
3785
- }
3786
- }
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);
3787
3894
  }
3788
- // Store records
3789
- this._records = data;
3895
+ return data;
3790
3896
  }
3791
- /**
3792
- * Get field by name
3793
- */ getField(name) {
3794
- 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
+ }
3795
3903
  }
3796
- /**
3797
- * Get field index by name
3798
- */ getFieldIndex(name) {
3799
- const field = this._fields.find((f)=>f.name === name);
3800
- return field ? field.index : -1;
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;
3801
3909
  }
3802
- /**
3803
- * Generate the pivotCacheDefinition XML
3804
- */ toDefinitionXml(recordsRelId) {
3805
- const cacheFieldNodes = this._fields.map((field)=>{
3806
- const sharedItemsAttrs = {};
3807
- const sharedItemChildren = [];
3808
- if (field.sharedItems.length > 0) {
3809
- // String field with shared items
3810
- const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
3811
- sharedItemsAttrs.count = String(total);
3812
- sharedItemsAttrs.containsString = '1';
3813
- if (field.hasBlank) {
3814
- sharedItemsAttrs.containsBlank = '1';
3815
- }
3816
- for (const item of field.sharedItems){
3817
- sharedItemChildren.push(createElement('s', {
3818
- v: item
3819
- }, []));
3820
- }
3821
- if (field.hasBlank) {
3822
- sharedItemChildren.push(createElement('m', {}, []));
3823
- }
3824
- } else if (field.isDate) {
3825
- sharedItemsAttrs.containsSemiMixedTypes = '0';
3826
- sharedItemsAttrs.containsString = '0';
3827
- sharedItemsAttrs.containsDate = '1';
3828
- sharedItemsAttrs.containsNonDate = '0';
3829
- if (field.hasBlank) {
3830
- sharedItemsAttrs.containsBlank = '1';
3831
- }
3832
- if (field.minDate) {
3833
- sharedItemsAttrs.minDate = this._formatDate(field.minDate);
3834
- }
3835
- if (field.maxDate) {
3836
- const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
3837
- sharedItemsAttrs.maxDate = this._formatDate(maxDate);
3838
- }
3839
- } else if (field.isNumeric) {
3840
- // Numeric field - use "0"/"1" for boolean attributes as Excel expects
3841
- sharedItemsAttrs.containsSemiMixedTypes = '0';
3842
- sharedItemsAttrs.containsString = '0';
3843
- sharedItemsAttrs.containsNumber = '1';
3844
- if (field.hasBlank) {
3845
- sharedItemsAttrs.containsBlank = '1';
3846
- }
3847
- // Check if all values are integers
3848
- if (field.minValue !== undefined && field.maxValue !== undefined) {
3849
- const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
3850
- if (isInteger) {
3851
- sharedItemsAttrs.containsInteger = '1';
3852
- }
3853
- sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
3854
- sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
3855
- }
3856
- } else if (field.hasBoolean) {
3857
- // Boolean-only field (no strings, no numbers)
3858
- if (field.hasBlank) {
3859
- sharedItemsAttrs.containsBlank = '1';
3860
- }
3861
- sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
3862
- sharedItemChildren.push(createElement('b', {
3863
- v: '0'
3864
- }, []));
3865
- sharedItemChildren.push(createElement('b', {
3866
- v: '1'
3867
- }, []));
3868
- if (field.hasBlank) {
3869
- sharedItemChildren.push(createElement('m', {}, []));
3870
- }
3871
- } else if (field.hasBlank) {
3872
- // Field that only contains blanks
3873
- sharedItemsAttrs.containsBlank = '1';
3874
- }
3875
- const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
3876
- const cacheFieldAttrs = {
3877
- name: field.name,
3878
- numFmtId: String(field.numFmtId ?? 0)
3879
- };
3880
- return createElement('cacheField', cacheFieldAttrs, [
3881
- sharedItemsNode
3882
- ]);
3883
- });
3884
- const cacheFieldsNode = createElement('cacheFields', {
3885
- count: String(this._fields.length)
3886
- }, cacheFieldNodes);
3887
- const worksheetSourceNode = createElement('worksheetSource', {
3888
- ref: this._sourceRange,
3889
- sheet: this._sourceSheet
3890
- }, []);
3891
- const cacheSourceNode = createElement('cacheSource', {
3892
- type: 'worksheet'
3893
- }, [
3894
- worksheetSourceNode
3895
- ]);
3896
- // Build attributes - align with Excel expectations
3897
- const definitionAttrs = {
3898
- xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3899
- 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3900
- 'r:id': recordsRelId
3901
- };
3902
- if (this._refreshOnLoad) {
3903
- definitionAttrs.refreshOnLoad = '1';
3904
- }
3905
- definitionAttrs.refreshedBy = 'User';
3906
- definitionAttrs.refreshedVersion = '8';
3907
- definitionAttrs.minRefreshableVersion = '3';
3908
- definitionAttrs.createdVersion = '8';
3909
- if (!this._saveData) {
3910
- definitionAttrs.saveData = '0';
3911
- definitionAttrs.recordCount = '0';
3912
- } else {
3913
- definitionAttrs.recordCount = String(this._recordCount);
3910
+ delete(path) {
3911
+ this._files.delete(path);
3912
+ this._deleted.add(path);
3913
+ if (this._entryNames) {
3914
+ this._entryNames.delete(path);
3914
3915
  }
3915
- const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
3916
- cacheSourceNode,
3917
- cacheFieldsNode
3918
- ]);
3919
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3920
- definitionNode
3921
- ])}`;
3922
3916
  }
3923
- /**
3924
- * Generate the pivotCacheRecords XML
3925
- */ toRecordsXml() {
3926
- const recordNodes = [];
3927
- for (const row of this._records){
3928
- const fieldNodes = [];
3929
- for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
3930
- const value = colIdx < row.length ? row[colIdx] : null;
3931
- if (value === null || value === undefined) {
3932
- // Missing value
3933
- const blankIndex = this._blankItemIndexMap.get(colIdx);
3934
- if (blankIndex !== undefined) {
3935
- fieldNodes.push(createElement('x', {
3936
- v: String(blankIndex)
3937
- }, []));
3938
- } else {
3939
- fieldNodes.push(createElement('m', {}, []));
3940
- }
3941
- } else if (typeof value === 'string') {
3942
- // String value - use index into sharedItems via O(1) Map lookup
3943
- const indexMap = this._sharedItemsIndexMap.get(colIdx);
3944
- const idx = indexMap?.get(value);
3945
- if (idx !== undefined) {
3946
- fieldNodes.push(createElement('x', {
3947
- v: String(idx)
3948
- }, []));
3949
- } else {
3950
- // Direct string value (shouldn't happen if cache is built correctly)
3951
- fieldNodes.push(createElement('s', {
3952
- v: value
3953
- }, []));
3954
- }
3955
- } else if (typeof value === 'number') {
3956
- if (this._fields[colIdx]?.isDate) {
3957
- const d = this._excelSerialToDate(value);
3958
- fieldNodes.push(createElement('d', {
3959
- v: this._formatDate(d)
3960
- }, []));
3961
- } else {
3962
- fieldNodes.push(createElement('n', {
3963
- v: String(value)
3964
- }, []));
3965
- }
3966
- } else if (typeof value === 'boolean') {
3967
- fieldNodes.push(createElement('b', {
3968
- v: value ? '1' : '0'
3969
- }, []));
3970
- } else if (value instanceof Date) {
3971
- fieldNodes.push(createElement('d', {
3972
- v: this._formatDate(value)
3973
- }, []));
3974
- } else {
3975
- // Unknown type, treat as missing
3976
- fieldNodes.push(createElement('m', {}, []));
3977
- }
3978
- }
3979
- recordNodes.push(createElement('r', {}, fieldNodes));
3980
- }
3981
- const recordsNode = createElement('pivotCacheRecords', {
3982
- xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3983
- 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3984
- count: String(this._recordCount)
3985
- }, recordNodes);
3986
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
3987
- recordsNode
3988
- ])}`;
3917
+ getText(path) {
3918
+ const data = this.get(path);
3919
+ if (!data) return undefined;
3920
+ return strFromU8(data);
3989
3921
  }
3990
- _formatDate(value) {
3991
- return value.toISOString().replace(/\.\d{3}Z$/, '');
3922
+ setText(path, content) {
3923
+ this.set(path, strToU8(content));
3992
3924
  }
3993
- _formatNumber(value) {
3994
- if (Number.isInteger(value)) {
3995
- 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);
3996
3930
  }
3997
- if (Math.abs(value) >= 1000000) {
3998
- return value.toFixed(16).replace(/0+$/, '').replace(/\.$/, '');
3931
+ for (const [path, content] of this._files){
3932
+ files.set(path, content);
3999
3933
  }
4000
- return String(value);
3934
+ return files;
4001
3935
  }
4002
- _excelSerialToDate(serial) {
4003
- // Excel epoch: December 31, 1899
4004
- const EXCEL_EPOCH = Date.UTC(1899, 11, 31);
4005
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
4006
- const adjusted = serial >= 60 ? serial - 1 : serial;
4007
- const ms = Math.round(adjusted * MS_PER_DAY);
4008
- return new Date(EXCEL_EPOCH + ms);
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;
4009
3946
  }
4010
3947
  }
4011
-
3948
+ const createZipStore = ()=>{
3949
+ return new EagerZipStore(new Map());
3950
+ };
4012
3951
  /**
4013
3952
  * Reads a ZIP file and returns a map of path -> content
4014
3953
  * @param data - ZIP file as Uint8Array
4015
3954
  * @returns Promise resolving to a map of file paths to contents
4016
- */ 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
+ }
4017
3960
  const isBun = typeof globalThis.Bun !== 'undefined';
4018
3961
  if (isBun) {
4019
3962
  try {
@@ -4022,7 +3965,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4022
3965
  for (const [path, content] of Object.entries(result)){
4023
3966
  files.set(path, content);
4024
3967
  }
4025
- return Promise.resolve(files);
3968
+ return Promise.resolve(new EagerZipStore(files));
4026
3969
  } catch (error) {
4027
3970
  return Promise.reject(error);
4028
3971
  }
@@ -4037,7 +3980,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4037
3980
  for (const [path, content] of Object.entries(result)){
4038
3981
  files.set(path, content);
4039
3982
  }
4040
- resolve(files);
3983
+ resolve(new EagerZipStore(files));
4041
3984
  });
4042
3985
  });
4043
3986
  };
@@ -4045,9 +3988,10 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4045
3988
  * Creates a ZIP file from a map of path -> content
4046
3989
  * @param files - Map of file paths to contents
4047
3990
  * @returns Promise resolving to ZIP file as Uint8Array
4048
- */ const writeZip = (files)=>{
3991
+ */ const writeZip = async (files)=>{
3992
+ const resolved = await files.toFiles();
4049
3993
  const zipData = {};
4050
- for (const [path, content] of files){
3994
+ for (const [path, content] of resolved){
4051
3995
  zipData[path] = content;
4052
3996
  }
4053
3997
  const isBun = typeof globalThis.Bun !== 'undefined';
@@ -4071,49 +4015,53 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4071
4015
  /**
4072
4016
  * Reads a file from the ZIP as a UTF-8 string
4073
4017
  */ const readZipText = (files, path)=>{
4074
- const data = files.get(path);
4075
- if (!data) return undefined;
4076
- return strFromU8(data);
4018
+ return files.getText(path);
4077
4019
  };
4078
4020
  /**
4079
4021
  * Writes a UTF-8 string to the ZIP files map
4080
4022
  */ const writeZipText = (files, path, content)=>{
4081
- files.set(path, strToU8(content));
4023
+ files.setText(path, content);
4082
4024
  };
4083
4025
 
4084
4026
  /**
4085
4027
  * Represents an Excel workbook (.xlsx file)
4086
4028
  */ class Workbook {
4087
4029
  constructor(){
4088
- this._files = new Map();
4030
+ this._files = createZipStore();
4089
4031
  this._sheets = new Map();
4090
4032
  this._sheetDefs = [];
4091
4033
  this._relationships = [];
4034
+ this._sharedStrings = null;
4035
+ this._styles = null;
4036
+ this._sharedStringsXml = null;
4037
+ this._stylesXml = null;
4038
+ this._lazy = true;
4092
4039
  this._dirty = false;
4093
- // Pivot table support
4094
- this._pivotTables = [];
4095
- this._pivotCaches = [];
4096
- this._nextCacheId = 5;
4097
- this._nextCacheFileIndex = 1;
4098
4040
  // Table support
4099
4041
  this._nextTableId = 1;
4042
+ // Pivot table support
4043
+ this._pivotTables = [];
4044
+ this._nextPivotTableId = 1;
4045
+ this._nextPivotCacheId = 1;
4100
4046
  // Date serialization handling
4101
4047
  this._dateHandling = 'jsDate';
4102
4048
  this._locale = 'fr-FR';
4103
- this._sharedStrings = new SharedStrings();
4104
- this._styles = Styles.createDefault();
4049
+ // Lazy init
4105
4050
  }
4106
4051
  /**
4107
4052
  * Load a workbook from a file path
4108
- */ static async fromFile(path) {
4053
+ */ static async fromFile(path, options = {}) {
4109
4054
  const data = await readFile(path);
4110
- return Workbook.fromBuffer(new Uint8Array(data));
4055
+ return Workbook.fromBuffer(new Uint8Array(data), options);
4111
4056
  }
4112
4057
  /**
4113
4058
  * Load a workbook from a buffer
4114
- */ static async fromBuffer(data) {
4059
+ */ static async fromBuffer(data, options = {}) {
4115
4060
  const workbook = new Workbook();
4116
- workbook._files = await readZip(data);
4061
+ workbook._lazy = options.lazy ?? true;
4062
+ workbook._files = await readZip(data, {
4063
+ lazy: workbook._lazy
4064
+ });
4117
4065
  // Parse workbook.xml for sheet definitions
4118
4066
  const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
4119
4067
  if (workbookXml) {
@@ -4125,15 +4073,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4125
4073
  workbook._parseRelationships(relsXml);
4126
4074
  }
4127
4075
  // Parse shared strings
4128
- const sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml');
4129
- if (sharedStringsXml) {
4130
- workbook._sharedStrings = SharedStrings.parse(sharedStringsXml);
4131
- }
4132
- // Parse styles
4133
- const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
4134
- if (stylesXml) {
4135
- workbook._styles = Styles.parse(stylesXml);
4136
- }
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;
4137
4079
  return workbook;
4138
4080
  }
4139
4081
  /**
@@ -4141,6 +4083,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4141
4083
  */ static create() {
4142
4084
  const workbook = new Workbook();
4143
4085
  workbook._dirty = true;
4086
+ workbook._lazy = false;
4087
+ workbook._sharedStrings = new SharedStrings();
4088
+ workbook._styles = Styles.createDefault();
4144
4089
  return workbook;
4145
4090
  }
4146
4091
  /**
@@ -4156,11 +4101,25 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4156
4101
  /**
4157
4102
  * Get shared strings table
4158
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
+ }
4159
4111
  return this._sharedStrings;
4160
4112
  }
4161
4113
  /**
4162
4114
  * Get styles
4163
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
+ }
4164
4123
  return this._styles;
4165
4124
  }
4166
4125
  /**
@@ -4191,6 +4150,72 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4191
4150
  return this._nextTableId++;
4192
4151
  }
4193
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
+ /**
4194
4219
  * Get a worksheet by name or index
4195
4220
  */ sheet(nameOrIndex) {
4196
4221
  let def;
@@ -4214,7 +4239,9 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4214
4239
  const sheetPath = `xl/${rel.target}`;
4215
4240
  const sheetXml = readZipText(this._files, sheetPath);
4216
4241
  if (sheetXml) {
4217
- worksheet.parse(sheetXml);
4242
+ worksheet.parse(sheetXml, {
4243
+ lazy: this._lazy
4244
+ });
4218
4245
  }
4219
4246
  }
4220
4247
  this._sheets.set(def.name, worksheet);
@@ -4525,100 +4552,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4525
4552
  return String(value);
4526
4553
  }
4527
4554
  /**
4528
- * Create a pivot table from source data.
4529
- *
4530
- * @param config - Pivot table configuration
4531
- * @returns PivotTable instance for fluent configuration
4532
- *
4533
- * @example
4534
- * ```typescript
4535
- * const pivot = wb.createPivotTable({
4536
- * name: 'SalesPivot',
4537
- * source: 'DataSheet!A1:D100',
4538
- * target: 'PivotSheet!A3',
4539
- * });
4540
- *
4541
- * pivot
4542
- * .addRowField('Region')
4543
- * .addColumnField('Product')
4544
- * .addValueField('Sales', 'sum', 'Total Sales');
4545
- * ```
4546
- */ createPivotTable(config) {
4547
- this._dirty = true;
4548
- // Parse source reference (Sheet!Range)
4549
- const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
4550
- // Parse target reference
4551
- const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
4552
- // Ensure target sheet exists
4553
- if (!this._sheetDefs.some((s)=>s.name === targetSheet)) {
4554
- this.addSheet(targetSheet);
4555
- }
4556
- // Parse target cell address
4557
- const targetAddr = parseAddress(targetCell);
4558
- // Get source worksheet and extract data
4559
- const sourceWs = this.sheet(sourceSheet);
4560
- const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
4561
- // Create pivot cache
4562
- const cacheId = this._nextCacheId++;
4563
- const cacheFileIndex = this._nextCacheFileIndex++;
4564
- const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
4565
- cache.setStyles(this._styles);
4566
- cache.buildFromData(headers, data);
4567
- // refreshOnLoad defaults to true; only disable if explicitly set to false
4568
- if (config.refreshOnLoad === false) {
4569
- cache.refreshOnLoad = false;
4570
- }
4571
- // saveData defaults to true; only disable if explicitly set to false
4572
- if (config.saveData === false) {
4573
- cache.saveData = false;
4574
- }
4575
- this._pivotCaches.push(cache);
4576
- // Create pivot table
4577
- const pivotTableIndex = this._pivotTables.length + 1;
4578
- const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex, cacheFileIndex);
4579
- // Set styles reference for number format resolution
4580
- pivotTable.setStyles(this._styles);
4581
- this._pivotTables.push(pivotTable);
4582
- return pivotTable;
4583
- }
4584
- /**
4585
- * Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
4586
- */ _parseSheetRef(ref) {
4587
- const match = ref.match(/^(.+?)!(.+)$/);
4588
- if (!match) {
4589
- throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
4590
- }
4591
- return {
4592
- sheetName: match[1],
4593
- range: match[2]
4594
- };
4595
- }
4596
- /**
4597
- * Extract headers and data from a source range
4598
- */ _extractSourceData(sheet, rangeStr) {
4599
- const range = parseRange(rangeStr);
4600
- const headers = [];
4601
- const data = [];
4602
- // First row is headers
4603
- for(let col = range.start.col; col <= range.end.col; col++){
4604
- const cell = sheet.cell(toAddress(range.start.row, col));
4605
- headers.push(String(cell.value ?? `Column${col + 1}`));
4606
- }
4607
- // Remaining rows are data
4608
- for(let row = range.start.row + 1; row <= range.end.row; row++){
4609
- const rowData = [];
4610
- for(let col = range.start.col; col <= range.end.col; col++){
4611
- const cell = sheet.cell(toAddress(row, col));
4612
- rowData.push(cell.value);
4613
- }
4614
- data.push(rowData);
4615
- }
4616
- return {
4617
- headers,
4618
- data
4619
- };
4620
- }
4621
- /**
4622
4555
  * Save the workbook to a file
4623
4556
  */ async toFile(path) {
4624
4557
  const buffer = await this.toBuffer();
@@ -4676,20 +4609,28 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4676
4609
  _updateFiles() {
4677
4610
  const relationshipInfo = this._buildRelationshipInfo();
4678
4611
  // Update workbook.xml
4679
- this._updateWorkbookXml(relationshipInfo.pivotCacheRelIds);
4612
+ this._updateWorkbookXml(relationshipInfo.pivotCacheRelByTarget);
4680
4613
  // Update relationships
4681
4614
  this._updateRelationshipsXml(relationshipInfo.relNodes);
4682
4615
  // Update content types
4683
4616
  this._updateContentTypes();
4684
4617
  // Update shared strings if modified
4685
- if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
4686
- 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);
4687
4624
  }
4688
4625
  // Update styles if modified or if file doesn't exist yet
4689
- if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
4690
- writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
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);
4691
4632
  }
4692
- // Update worksheets (needed for pivot table targets)
4633
+ // Update worksheets
4693
4634
  for (const [name, worksheet] of this._sheets){
4694
4635
  if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
4695
4636
  const def = this._sheetDefs.find((s)=>s.name === name);
@@ -4702,15 +4643,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4702
4643
  }
4703
4644
  }
4704
4645
  }
4705
- // Update pivot tables
4706
- if (this._pivotTables.length > 0) {
4707
- this._updatePivotTableFiles();
4708
- }
4709
4646
  // Update tables (sets table rel IDs for tableParts)
4710
4647
  this._updateTableFiles();
4648
+ // Update pivot tables (sets pivot rel IDs for pivotTableParts)
4649
+ this._updatePivotFiles();
4711
4650
  // Update worksheets to align tableParts with relationship IDs
4712
4651
  for (const [name, worksheet] of this._sheets){
4713
- if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
4652
+ if (worksheet.dirty || this._dirty || worksheet.tables.length > 0 || this._pivotTables.length > 0) {
4714
4653
  const def = this._sheetDefs.find((s)=>s.name === name);
4715
4654
  if (def) {
4716
4655
  const rel = this._relationships.find((r)=>r.id === def.rId);
@@ -4722,7 +4661,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4722
4661
  }
4723
4662
  }
4724
4663
  }
4725
- _updateWorkbookXml(pivotCacheRelIds) {
4664
+ _updateWorkbookXml(pivotCacheRelByTarget) {
4726
4665
  const sheetNodes = this._sheetDefs.map((def)=>createElement('sheet', {
4727
4666
  name: def.name,
4728
4667
  sheetId: String(def.sheetId),
@@ -4732,19 +4671,20 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4732
4671
  const children = [
4733
4672
  sheetsNode
4734
4673
  ];
4735
- // Add pivot caches if any
4736
- if (this._pivotCaches.length > 0) {
4737
- const pivotCacheNodes = this._pivotCaches.map((cache)=>{
4738
- const cacheRelId = pivotCacheRelIds.get(cache.cacheId);
4739
- if (!cacheRelId) {
4740
- throw new Error(`Missing pivot cache relationship ID for cache ${cache.cacheId}`);
4741
- }
4742
- return createElement('pivotCache', {
4743
- cacheId: String(cache.cacheId),
4744
- 'r:id': cacheRelId
4745
- }, []);
4746
- });
4747
- children.push(createElement('pivotCaches', {}, pivotCacheNodes));
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
+ }
4748
4688
  }
4749
4689
  const workbookNode = createElement('workbook', {
4750
4690
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
@@ -4770,6 +4710,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4770
4710
  Type: rel.type,
4771
4711
  Target: rel.target
4772
4712
  }, []));
4713
+ const pivotCacheRelByTarget = new Map();
4773
4714
  const reservedRelIds = new Set(relNodes.map((node)=>getAttr(node, 'Id') || '').filter(Boolean));
4774
4715
  let nextRelId = Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
4775
4716
  const allocateRelId = ()=>{
@@ -4782,7 +4723,8 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4782
4723
  return id;
4783
4724
  };
4784
4725
  // Add shared strings relationship if needed
4785
- if (this._sharedStrings.count > 0) {
4726
+ const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
4727
+ if (shouldIncludeSharedStrings) {
4786
4728
  const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
4787
4729
  if (!hasSharedStrings) {
4788
4730
  relNodes.push(createElement('Relationship', {
@@ -4801,23 +4743,32 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4801
4743
  Target: 'styles.xml'
4802
4744
  }, []));
4803
4745
  }
4804
- // Add pivot cache relationships
4805
- const pivotCacheRelIds = new Map();
4806
- for (const cache of this._pivotCaches){
4807
- const id = allocateRelId();
4808
- pivotCacheRelIds.set(cache.cacheId, id);
4809
- relNodes.push(createElement('Relationship', {
4810
- Id: id,
4811
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
4812
- Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`
4813
- }, []));
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
+ }
4814
4764
  }
4815
4765
  return {
4816
4766
  relNodes,
4817
- pivotCacheRelIds
4767
+ pivotCacheRelByTarget
4818
4768
  };
4819
4769
  }
4820
4770
  _updateContentTypes() {
4771
+ const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
4821
4772
  const types = [
4822
4773
  createElement('Default', {
4823
4774
  Extension: 'rels',
@@ -4837,7 +4788,7 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4837
4788
  }, [])
4838
4789
  ];
4839
4790
  // Add shared strings if present
4840
- if (this._sharedStrings.count > 0) {
4791
+ if (shouldIncludeSharedStrings) {
4841
4792
  types.push(createElement('Override', {
4842
4793
  PartName: '/xl/sharedStrings.xml',
4843
4794
  ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'
@@ -4853,24 +4804,6 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4853
4804
  }, []));
4854
4805
  }
4855
4806
  }
4856
- // Add pivot cache definitions and records
4857
- for (const cache of this._pivotCaches){
4858
- types.push(createElement('Override', {
4859
- PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
4860
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
4861
- }, []));
4862
- types.push(createElement('Override', {
4863
- PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,
4864
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
4865
- }, []));
4866
- }
4867
- // Add pivot tables
4868
- for (const pivotTable of this._pivotTables){
4869
- types.push(createElement('Override', {
4870
- PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,
4871
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
4872
- }, []));
4873
- }
4874
4807
  // Add tables
4875
4808
  let tableIndex = 1;
4876
4809
  for (const def of this._sheetDefs){
@@ -4885,6 +4818,21 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4885
4818
  }
4886
4819
  }
4887
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
+ }
4888
4836
  const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
4889
4837
  const existingKeys = new Set(types.map((t)=>{
4890
4838
  if ('Default' in t) {
@@ -4940,66 +4888,35 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
4940
4888
  }
4941
4889
  }
4942
4890
  /**
4943
- * Generate all pivot table related files
4944
- */ _updatePivotTableFiles() {
4945
- // Track which sheets have pivot tables for their .rels files
4946
- const sheetPivotTables = new Map();
4947
- for (const pivotTable of this._pivotTables){
4948
- const sheetName = pivotTable.targetSheet;
4949
- if (!sheetPivotTables.has(sheetName)) {
4950
- sheetPivotTables.set(sheetName, []);
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++;
4951
4908
  }
4952
- sheetPivotTables.get(sheetName).push(pivotTable);
4953
- }
4954
- // Generate pivot cache files
4955
- for(let i = 0; i < this._pivotCaches.length; i++){
4956
- const cache = this._pivotCaches[i];
4957
- // Pivot cache definition
4958
- const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;
4959
- writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
4960
- // Pivot cache records
4961
- const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;
4962
- writeZipText(this._files, recordsPath, cache.toRecordsXml());
4963
- // Pivot cache definition relationships (link to records)
4964
- const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;
4965
- const cacheRels = createElement('Relationships', {
4966
- xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
4967
- }, [
4968
- createElement('Relationship', {
4969
- Id: 'rId1',
4970
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
4971
- Target: `pivotCacheRecords${cache.fileIndex}.xml`
4972
- }, [])
4973
- ]);
4974
- writeZipText(this._files, cacheRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
4975
- cacheRels
4976
- ])}`);
4909
+ sheetTables.set(def.name, tableInfos);
4977
4910
  }
4978
- // Generate pivot table files
4979
- for(let i = 0; i < this._pivotTables.length; i++){
4980
- const pivotTable = this._pivotTables[i];
4981
- const ptIdx = pivotTable.index;
4982
- // Pivot table definition
4983
- const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
4984
- writeZipText(this._files, ptPath, pivotTable.toXml());
4985
- // Pivot table relationships (link to cache definition)
4986
- const cacheIdx = pivotTable.cacheFileIndex;
4987
- const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
4988
- const ptRels = createElement('Relationships', {
4989
- xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
4990
- }, [
4991
- createElement('Relationship', {
4992
- Id: 'rId1',
4993
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
4994
- Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`
4995
- }, [])
4996
- ]);
4997
- writeZipText(this._files, ptRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
4998
- ptRels
4999
- ])}`);
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
+ }
5000
4917
  }
5001
- // Generate worksheet relationships for pivot tables
5002
- for (const [sheetName, pivotTables] of sheetPivotTables){
4918
+ // Generate worksheet relationships for tables
4919
+ for (const [sheetName, tableInfos] of sheetTables){
5003
4920
  const def = this._sheetDefs.find((s)=>s.name === sheetName);
5004
4921
  if (!def) continue;
5005
4922
  const rel = this._relationships.find((r)=>r.id === def.rId);
@@ -5007,11 +4924,13 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5007
4924
  // Extract sheet file name from target path
5008
4925
  const sheetFileName = rel.target.split('/').pop();
5009
4926
  const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
4927
+ // Check if there are already pivot table relationships for this sheet
5010
4928
  const existingRelsXml = readZipText(this._files, sheetRelsPath);
5011
- let relNodes = [];
5012
4929
  let nextRelId = 1;
4930
+ const relNodes = [];
5013
4931
  const reservedRelIds = new Set();
5014
4932
  if (existingRelsXml) {
4933
+ // Parse existing rels and find max rId
5015
4934
  const parsed = parseXml(existingRelsXml);
5016
4935
  const relsElement = findElement(parsed, 'Relationships');
5017
4936
  if (relsElement) {
@@ -5040,16 +4959,29 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5040
4959
  reservedRelIds.add(id);
5041
4960
  return id;
5042
4961
  };
5043
- for (const pt of pivotTables){
5044
- const target = `../pivotTables/pivotTable${pt.index}.xml`;
5045
- const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' && getAttr(node, 'Target') === target);
5046
- if (existing) continue;
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);
5047
4975
  relNodes.push(createElement('Relationship', {
5048
- Id: allocateRelId(),
5049
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
4976
+ Id: id,
4977
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
5050
4978
  Target: target
5051
4979
  }, []));
5052
4980
  }
4981
+ const worksheet = this._sheets.get(sheetName);
4982
+ if (worksheet) {
4983
+ worksheet.setTableRelIds(tableRelIds);
4984
+ }
5053
4985
  const sheetRels = createElement('Relationships', {
5054
4986
  xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
5055
4987
  }, relNodes);
@@ -5059,54 +4991,45 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5059
4991
  }
5060
4992
  }
5061
4993
  /**
5062
- * Generate all table related files
5063
- */ _updateTableFiles() {
5064
- // Collect all tables with their global indices
5065
- let globalTableIndex = 1;
5066
- const sheetTables = new Map();
5067
- for (const def of this._sheetDefs){
5068
- const worksheet = this._sheets.get(def.name);
5069
- if (!worksheet) continue;
5070
- const tables = worksheet.tables;
5071
- if (tables.length === 0) continue;
5072
- const tableInfos = [];
5073
- for (const table of tables){
5074
- tableInfos.push({
5075
- table,
5076
- globalIndex: globalTableIndex
5077
- });
5078
- globalTableIndex++;
5079
- }
5080
- sheetTables.set(def.name, tableInfos);
5081
- }
5082
- // Generate table files
5083
- for (const [, tableInfos] of sheetTables){
5084
- for (const { table, globalIndex } of tableInfos){
5085
- const tablePath = `xl/tables/table${globalIndex}.xml`;
5086
- writeZipText(this._files, tablePath, table.toXml());
5087
- }
4994
+ * Generate pivot cache/table parts and worksheet relationships.
4995
+ */ _updatePivotFiles() {
4996
+ if (this._pivotTables.length === 0) {
4997
+ return;
5088
4998
  }
5089
- // Generate worksheet relationships for tables
5090
- 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){
5091
5017
  const def = this._sheetDefs.find((s)=>s.name === sheetName);
5092
5018
  if (!def) continue;
5093
5019
  const rel = this._relationships.find((r)=>r.id === def.rId);
5094
5020
  if (!rel) continue;
5095
- // Extract sheet file name from target path
5096
5021
  const sheetFileName = rel.target.split('/').pop();
5022
+ if (!sheetFileName) continue;
5097
5023
  const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
5098
- // Check if there are already pivot table relationships for this sheet
5099
5024
  const existingRelsXml = readZipText(this._files, sheetRelsPath);
5100
5025
  let nextRelId = 1;
5101
5026
  const relNodes = [];
5102
5027
  const reservedRelIds = new Set();
5103
5028
  if (existingRelsXml) {
5104
- // Parse existing rels and find max rId
5105
5029
  const parsed = parseXml(existingRelsXml);
5106
5030
  const relsElement = findElement(parsed, 'Relationships');
5107
5031
  if (relsElement) {
5108
- const existingRelNodes = getChildren(relsElement, 'Relationships');
5109
- for (const relNode of existingRelNodes){
5032
+ for (const relNode of getChildren(relsElement, 'Relationships')){
5110
5033
  if ('Relationship' in relNode) {
5111
5034
  relNodes.push(relNode);
5112
5035
  const id = getAttr(relNode, 'Id');
@@ -5130,28 +5053,26 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5130
5053
  reservedRelIds.add(id);
5131
5054
  return id;
5132
5055
  };
5133
- // Add table relationships
5134
- const tableRelIds = [];
5135
- for (const { globalIndex } of tableInfos){
5136
- const target = `../tables/table${globalIndex}.xml`;
5137
- const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
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);
5138
5060
  if (existing) {
5139
- const existingRel = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
5140
- const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
5141
- tableRelIds.push(existingId ?? allocateRelId());
5061
+ const existingId = getAttr(existing, 'Id');
5062
+ pivotRelIds.push(existingId ?? allocateRelId());
5142
5063
  continue;
5143
5064
  }
5144
5065
  const id = allocateRelId();
5145
- tableRelIds.push(id);
5066
+ pivotRelIds.push(id);
5146
5067
  relNodes.push(createElement('Relationship', {
5147
5068
  Id: id,
5148
- Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
5069
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
5149
5070
  Target: target
5150
5071
  }, []));
5151
5072
  }
5152
5073
  const worksheet = this._sheets.get(sheetName);
5153
5074
  if (worksheet) {
5154
- worksheet.setTableRelIds(tableRelIds);
5075
+ worksheet.setPivotTableRelIds(pivotRelIds);
5155
5076
  }
5156
5077
  const sheetRels = createElement('Relationships', {
5157
5078
  xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
@@ -5163,4 +5084,4 @@ const shouldEscapeXmlAttr = (tagName, attrName)=>{
5163
5084
  }
5164
5085
  }
5165
5086
 
5166
- 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 };