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