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