@niicojs/excel 0.2.2 → 0.2.4
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/README.md +373 -5
- package/dist/index.cjs +507 -25
- package/dist/index.d.cts +172 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +172 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +504 -26
- package/package.json +1 -1
- package/src/index.ts +17 -1
- package/src/pivot-cache.ts +11 -1
- package/src/pivot-table.ts +122 -12
- package/src/range.ts +15 -2
- package/src/styles.ts +60 -1
- package/src/types.ts +62 -0
- package/src/utils/address.ts +4 -1
- package/src/utils/xml.ts +0 -7
- package/src/workbook.ts +18 -0
- package/src/worksheet.ts +343 -4
package/dist/index.cjs
CHANGED
|
@@ -40,8 +40,10 @@ var fflate = require('fflate');
|
|
|
40
40
|
if (!match) {
|
|
41
41
|
throw new Error(`Invalid cell address: ${address}`);
|
|
42
42
|
}
|
|
43
|
+
const rowNumber = +match[2];
|
|
44
|
+
if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);
|
|
43
45
|
const col = letterToCol(match[1].toUpperCase());
|
|
44
|
-
const row =
|
|
46
|
+
const row = rowNumber - 1; // Convert to 0-based
|
|
45
47
|
return {
|
|
46
48
|
row,
|
|
47
49
|
col
|
|
@@ -103,6 +105,12 @@ var fflate = require('fflate');
|
|
|
103
105
|
}
|
|
104
106
|
};
|
|
105
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Checks if an address is within a range
|
|
110
|
+
*/ const isInRange = (addr, range)=>{
|
|
111
|
+
const norm = normalizeRange(range);
|
|
112
|
+
return addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col;
|
|
113
|
+
};
|
|
106
114
|
|
|
107
115
|
// Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
|
|
108
116
|
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
|
|
@@ -435,12 +443,23 @@ _computedKey = Symbol.iterator;
|
|
|
435
443
|
/**
|
|
436
444
|
* Get all values in the range as a 2D array
|
|
437
445
|
*/ get values() {
|
|
446
|
+
return this.getValues();
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get all values in the range as a 2D array with options
|
|
450
|
+
*/ getValues(options = {}) {
|
|
451
|
+
const { createMissing = true } = options;
|
|
438
452
|
const result = [];
|
|
439
453
|
for(let r = this._range.start.row; r <= this._range.end.row; r++){
|
|
440
454
|
const row = [];
|
|
441
455
|
for(let c = this._range.start.col; c <= this._range.end.col; c++){
|
|
442
|
-
|
|
443
|
-
|
|
456
|
+
if (createMissing) {
|
|
457
|
+
const cell = this._worksheet.cell(r, c);
|
|
458
|
+
row.push(cell.value);
|
|
459
|
+
} else {
|
|
460
|
+
const cell = this._worksheet.getCellIfExists(r, c);
|
|
461
|
+
row.push(cell?.value ?? null);
|
|
462
|
+
}
|
|
444
463
|
}
|
|
445
464
|
result.push(row);
|
|
446
465
|
}
|
|
@@ -613,6 +632,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
613
632
|
this._dirty = false;
|
|
614
633
|
this._mergedCells = new Set();
|
|
615
634
|
this._sheetData = [];
|
|
635
|
+
this._columnWidths = new Map();
|
|
636
|
+
this._rowHeights = new Map();
|
|
637
|
+
this._frozenPane = null;
|
|
638
|
+
this._dataBoundsCache = null;
|
|
639
|
+
this._boundsDirty = true;
|
|
616
640
|
this._workbook = workbook;
|
|
617
641
|
this._name = name;
|
|
618
642
|
}
|
|
@@ -639,12 +663,49 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
639
663
|
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
640
664
|
if (!worksheet) return;
|
|
641
665
|
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
666
|
+
// Parse sheet views (freeze panes)
|
|
667
|
+
const sheetViews = findElement(worksheetChildren, 'sheetViews');
|
|
668
|
+
if (sheetViews) {
|
|
669
|
+
const viewChildren = getChildren(sheetViews, 'sheetViews');
|
|
670
|
+
const sheetView = findElement(viewChildren, 'sheetView');
|
|
671
|
+
if (sheetView) {
|
|
672
|
+
const sheetViewChildren = getChildren(sheetView, 'sheetView');
|
|
673
|
+
const pane = findElement(sheetViewChildren, 'pane');
|
|
674
|
+
if (pane && getAttr(pane, 'state') === 'frozen') {
|
|
675
|
+
const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);
|
|
676
|
+
const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);
|
|
677
|
+
if (xSplit > 0 || ySplit > 0) {
|
|
678
|
+
this._frozenPane = {
|
|
679
|
+
row: ySplit,
|
|
680
|
+
col: xSplit
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
642
686
|
// Parse sheet data (cells)
|
|
643
687
|
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
644
688
|
if (sheetData) {
|
|
645
689
|
this._sheetData = getChildren(sheetData, 'sheetData');
|
|
646
690
|
this._parseSheetData(this._sheetData);
|
|
647
691
|
}
|
|
692
|
+
// Parse column widths
|
|
693
|
+
const cols = findElement(worksheetChildren, 'cols');
|
|
694
|
+
if (cols) {
|
|
695
|
+
const colChildren = getChildren(cols, 'cols');
|
|
696
|
+
for (const col of colChildren){
|
|
697
|
+
if (!('col' in col)) continue;
|
|
698
|
+
const min = parseInt(getAttr(col, 'min') || '0', 10);
|
|
699
|
+
const max = parseInt(getAttr(col, 'max') || '0', 10);
|
|
700
|
+
const width = parseFloat(getAttr(col, 'width') || '0');
|
|
701
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
702
|
+
if (min > 0 && max > 0) {
|
|
703
|
+
for(let idx = min; idx <= max; idx++){
|
|
704
|
+
this._columnWidths.set(idx - 1, width);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
648
709
|
// Parse merged cells
|
|
649
710
|
const mergeCells = findElement(worksheetChildren, 'mergeCells');
|
|
650
711
|
if (mergeCells) {
|
|
@@ -664,6 +725,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
664
725
|
*/ _parseSheetData(rows) {
|
|
665
726
|
for (const rowNode of rows){
|
|
666
727
|
if (!('row' in rowNode)) continue;
|
|
728
|
+
const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;
|
|
729
|
+
const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');
|
|
730
|
+
if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {
|
|
731
|
+
this._rowHeights.set(rowIndex, rowHeight);
|
|
732
|
+
}
|
|
667
733
|
const rowChildren = getChildren(rowNode, 'row');
|
|
668
734
|
for (const cellNode of rowChildren){
|
|
669
735
|
if (!('c' in cellNode)) continue;
|
|
@@ -675,6 +741,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
675
741
|
this._cells.set(ref, cell);
|
|
676
742
|
}
|
|
677
743
|
}
|
|
744
|
+
this._boundsDirty = true;
|
|
678
745
|
}
|
|
679
746
|
/**
|
|
680
747
|
* Parse a cell XML node to CellData
|
|
@@ -761,9 +828,17 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
761
828
|
if (!cell) {
|
|
762
829
|
cell = new Cell(this, row, c);
|
|
763
830
|
this._cells.set(address, cell);
|
|
831
|
+
this._boundsDirty = true;
|
|
764
832
|
}
|
|
765
833
|
return cell;
|
|
766
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Get an existing cell without creating it.
|
|
837
|
+
*/ getCellIfExists(rowOrAddress, col) {
|
|
838
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
839
|
+
const address = toAddress(row, c);
|
|
840
|
+
return this._cells.get(address);
|
|
841
|
+
}
|
|
767
842
|
range(startRowOrRange, startCol, endRow, endCol) {
|
|
768
843
|
let rangeAddr;
|
|
769
844
|
if (typeof startRowOrRange === 'string') {
|
|
@@ -823,6 +898,182 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
823
898
|
return this._cells;
|
|
824
899
|
}
|
|
825
900
|
/**
|
|
901
|
+
* Set a column width (0-based index or column letter)
|
|
902
|
+
*/ setColumnWidth(col, width) {
|
|
903
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
904
|
+
throw new Error('Column width must be a positive number');
|
|
905
|
+
}
|
|
906
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
907
|
+
if (colIndex < 0) {
|
|
908
|
+
throw new Error(`Invalid column: ${col}`);
|
|
909
|
+
}
|
|
910
|
+
this._columnWidths.set(colIndex, width);
|
|
911
|
+
this._dirty = true;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Get a column width if set
|
|
915
|
+
*/ getColumnWidth(col) {
|
|
916
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
917
|
+
return this._columnWidths.get(colIndex);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Set a row height (0-based index)
|
|
921
|
+
*/ setRowHeight(row, height) {
|
|
922
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
923
|
+
throw new Error('Row height must be a positive number');
|
|
924
|
+
}
|
|
925
|
+
if (row < 0) {
|
|
926
|
+
throw new Error('Row index must be >= 0');
|
|
927
|
+
}
|
|
928
|
+
this._rowHeights.set(row, height);
|
|
929
|
+
this._dirty = true;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Get a row height if set
|
|
933
|
+
*/ getRowHeight(row) {
|
|
934
|
+
return this._rowHeights.get(row);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Freeze panes at a given row/column split (counts from top-left)
|
|
938
|
+
*/ freezePane(rowSplit, colSplit) {
|
|
939
|
+
if (rowSplit < 0 || colSplit < 0) {
|
|
940
|
+
throw new Error('Freeze pane splits must be >= 0');
|
|
941
|
+
}
|
|
942
|
+
if (rowSplit === 0 && colSplit === 0) {
|
|
943
|
+
this._frozenPane = null;
|
|
944
|
+
} else {
|
|
945
|
+
this._frozenPane = {
|
|
946
|
+
row: rowSplit,
|
|
947
|
+
col: colSplit
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
this._dirty = true;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Get current frozen pane configuration
|
|
954
|
+
*/ getFrozenPane() {
|
|
955
|
+
return this._frozenPane ? {
|
|
956
|
+
...this._frozenPane
|
|
957
|
+
} : null;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Convert sheet data to an array of JSON objects.
|
|
961
|
+
*
|
|
962
|
+
* @param config - Configuration options
|
|
963
|
+
* @returns Array of objects where keys are field names and values are cell values
|
|
964
|
+
*
|
|
965
|
+
* @example
|
|
966
|
+
* ```typescript
|
|
967
|
+
* // Using first row as headers
|
|
968
|
+
* const data = sheet.toJson();
|
|
969
|
+
*
|
|
970
|
+
* // Using custom field names
|
|
971
|
+
* const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
|
|
972
|
+
*
|
|
973
|
+
* // Starting from a specific row/column
|
|
974
|
+
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
975
|
+
* ```
|
|
976
|
+
*/ toJson(config = {}) {
|
|
977
|
+
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
|
|
978
|
+
// Get the bounds of data in the sheet
|
|
979
|
+
const bounds = this._getDataBounds();
|
|
980
|
+
if (!bounds) {
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
const effectiveEndRow = endRow ?? bounds.maxRow;
|
|
984
|
+
const effectiveEndCol = endCol ?? bounds.maxCol;
|
|
985
|
+
// Determine field names
|
|
986
|
+
let fieldNames;
|
|
987
|
+
let dataStartRow;
|
|
988
|
+
if (fields) {
|
|
989
|
+
// Use provided field names, data starts at startRow
|
|
990
|
+
fieldNames = fields;
|
|
991
|
+
dataStartRow = startRow;
|
|
992
|
+
} else {
|
|
993
|
+
// Use first row as headers
|
|
994
|
+
fieldNames = [];
|
|
995
|
+
for(let col = startCol; col <= effectiveEndCol; col++){
|
|
996
|
+
const cell = this._cells.get(toAddress(startRow, col));
|
|
997
|
+
const value = cell?.value;
|
|
998
|
+
fieldNames.push(value != null ? String(value) : `column${col}`);
|
|
999
|
+
}
|
|
1000
|
+
dataStartRow = startRow + 1;
|
|
1001
|
+
}
|
|
1002
|
+
// Read data rows
|
|
1003
|
+
const result = [];
|
|
1004
|
+
for(let row = dataStartRow; row <= effectiveEndRow; row++){
|
|
1005
|
+
const obj = {};
|
|
1006
|
+
let hasData = false;
|
|
1007
|
+
for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
|
|
1008
|
+
const col = startCol + colOffset;
|
|
1009
|
+
const cell = this._cells.get(toAddress(row, col));
|
|
1010
|
+
let value = cell?.value ?? null;
|
|
1011
|
+
if (value instanceof Date) {
|
|
1012
|
+
value = this._serializeDate(value, dateHandling, cell);
|
|
1013
|
+
}
|
|
1014
|
+
if (value !== null) {
|
|
1015
|
+
hasData = true;
|
|
1016
|
+
}
|
|
1017
|
+
const fieldName = fieldNames[colOffset];
|
|
1018
|
+
if (fieldName) {
|
|
1019
|
+
obj[fieldName] = value;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Stop on empty row if configured
|
|
1023
|
+
if (stopOnEmptyRow && !hasData) {
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
result.push(obj);
|
|
1027
|
+
}
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
_serializeDate(value, dateHandling, cell) {
|
|
1031
|
+
if (dateHandling === 'excelSerial') {
|
|
1032
|
+
return cell?._jsDateToExcel(value) ?? value;
|
|
1033
|
+
}
|
|
1034
|
+
if (dateHandling === 'isoString') {
|
|
1035
|
+
return value.toISOString();
|
|
1036
|
+
}
|
|
1037
|
+
return value;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
1041
|
+
*/ _getDataBounds() {
|
|
1042
|
+
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
1043
|
+
return this._dataBoundsCache;
|
|
1044
|
+
}
|
|
1045
|
+
if (this._cells.size === 0) {
|
|
1046
|
+
this._dataBoundsCache = null;
|
|
1047
|
+
this._boundsDirty = false;
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
let minRow = Infinity;
|
|
1051
|
+
let maxRow = -Infinity;
|
|
1052
|
+
let minCol = Infinity;
|
|
1053
|
+
let maxCol = -Infinity;
|
|
1054
|
+
for (const cell of this._cells.values()){
|
|
1055
|
+
if (cell.value !== null) {
|
|
1056
|
+
minRow = Math.min(minRow, cell.row);
|
|
1057
|
+
maxRow = Math.max(maxRow, cell.row);
|
|
1058
|
+
minCol = Math.min(minCol, cell.col);
|
|
1059
|
+
maxCol = Math.max(maxCol, cell.col);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (minRow === Infinity) {
|
|
1063
|
+
this._dataBoundsCache = null;
|
|
1064
|
+
this._boundsDirty = false;
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
this._dataBoundsCache = {
|
|
1068
|
+
minRow,
|
|
1069
|
+
maxRow,
|
|
1070
|
+
minCol,
|
|
1071
|
+
maxCol
|
|
1072
|
+
};
|
|
1073
|
+
this._boundsDirty = false;
|
|
1074
|
+
return this._dataBoundsCache;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
826
1077
|
* Generate XML for this worksheet
|
|
827
1078
|
*/ toXml() {
|
|
828
1079
|
// Build sheetData from cells
|
|
@@ -834,6 +1085,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
834
1085
|
}
|
|
835
1086
|
rowMap.get(row).push(cell);
|
|
836
1087
|
}
|
|
1088
|
+
for (const rowIdx of this._rowHeights.keys()){
|
|
1089
|
+
if (!rowMap.has(rowIdx)) {
|
|
1090
|
+
rowMap.set(rowIdx, []);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
837
1093
|
// Sort rows and cells
|
|
838
1094
|
const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
|
|
839
1095
|
const rowNodes = [];
|
|
@@ -844,16 +1100,71 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
844
1100
|
const cellNode = this._buildCellNode(cell);
|
|
845
1101
|
cellNodes.push(cellNode);
|
|
846
1102
|
}
|
|
847
|
-
const
|
|
1103
|
+
const rowAttrs = {
|
|
848
1104
|
r: String(rowIdx + 1)
|
|
849
|
-
}
|
|
1105
|
+
};
|
|
1106
|
+
const rowHeight = this._rowHeights.get(rowIdx);
|
|
1107
|
+
if (rowHeight !== undefined) {
|
|
1108
|
+
rowAttrs.ht = String(rowHeight);
|
|
1109
|
+
rowAttrs.customHeight = '1';
|
|
1110
|
+
}
|
|
1111
|
+
const rowNode = createElement('row', rowAttrs, cellNodes);
|
|
850
1112
|
rowNodes.push(rowNode);
|
|
851
1113
|
}
|
|
852
1114
|
const sheetDataNode = createElement('sheetData', {}, rowNodes);
|
|
853
1115
|
// Build worksheet structure
|
|
854
|
-
const worksheetChildren = [
|
|
855
|
-
|
|
856
|
-
|
|
1116
|
+
const worksheetChildren = [];
|
|
1117
|
+
// Sheet views (freeze panes)
|
|
1118
|
+
if (this._frozenPane) {
|
|
1119
|
+
const paneAttrs = {
|
|
1120
|
+
state: 'frozen'
|
|
1121
|
+
};
|
|
1122
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
1123
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
1124
|
+
if (this._frozenPane.col > 0) {
|
|
1125
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
1126
|
+
}
|
|
1127
|
+
if (this._frozenPane.row > 0) {
|
|
1128
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
1129
|
+
}
|
|
1130
|
+
let activePane = 'bottomRight';
|
|
1131
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
1132
|
+
activePane = 'bottomLeft';
|
|
1133
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
1134
|
+
activePane = 'topRight';
|
|
1135
|
+
}
|
|
1136
|
+
paneAttrs.activePane = activePane;
|
|
1137
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
1138
|
+
const selectionNode = createElement('selection', {
|
|
1139
|
+
pane: activePane,
|
|
1140
|
+
activeCell: topLeftCell,
|
|
1141
|
+
sqref: topLeftCell
|
|
1142
|
+
}, []);
|
|
1143
|
+
const sheetViewNode = createElement('sheetView', {
|
|
1144
|
+
workbookViewId: '0'
|
|
1145
|
+
}, [
|
|
1146
|
+
paneNode,
|
|
1147
|
+
selectionNode
|
|
1148
|
+
]);
|
|
1149
|
+
worksheetChildren.push(createElement('sheetViews', {}, [
|
|
1150
|
+
sheetViewNode
|
|
1151
|
+
]));
|
|
1152
|
+
}
|
|
1153
|
+
// Column widths
|
|
1154
|
+
if (this._columnWidths.size > 0) {
|
|
1155
|
+
const colNodes = [];
|
|
1156
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b)=>a[0] - b[0]);
|
|
1157
|
+
for (const [colIndex, width] of entries){
|
|
1158
|
+
colNodes.push(createElement('col', {
|
|
1159
|
+
min: String(colIndex + 1),
|
|
1160
|
+
max: String(colIndex + 1),
|
|
1161
|
+
width: String(width),
|
|
1162
|
+
customWidth: '1'
|
|
1163
|
+
}, []));
|
|
1164
|
+
}
|
|
1165
|
+
worksheetChildren.push(createElement('cols', {}, colNodes));
|
|
1166
|
+
}
|
|
1167
|
+
worksheetChildren.push(sheetDataNode);
|
|
857
1168
|
// Add merged cells if any
|
|
858
1169
|
if (this._mergedCells.size > 0) {
|
|
859
1170
|
const mergeCellNodes = [];
|
|
@@ -1040,6 +1351,40 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1040
1351
|
* Manages the styles (xl/styles.xml)
|
|
1041
1352
|
*/ class Styles {
|
|
1042
1353
|
/**
|
|
1354
|
+
* Generate a deterministic cache key for a style object.
|
|
1355
|
+
* More efficient than JSON.stringify as it avoids the overhead of
|
|
1356
|
+
* full JSON serialization and produces a consistent key regardless
|
|
1357
|
+
* of property order.
|
|
1358
|
+
*/ _getStyleKey(style) {
|
|
1359
|
+
// Use a delimiter that won't appear in values
|
|
1360
|
+
const SEP = '\x00';
|
|
1361
|
+
// Build key from all style properties in a fixed order
|
|
1362
|
+
const parts = [
|
|
1363
|
+
style.bold ? '1' : '0',
|
|
1364
|
+
style.italic ? '1' : '0',
|
|
1365
|
+
style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',
|
|
1366
|
+
style.strike ? '1' : '0',
|
|
1367
|
+
style.fontSize?.toString() ?? '',
|
|
1368
|
+
style.fontName ?? '',
|
|
1369
|
+
style.fontColor ?? '',
|
|
1370
|
+
style.fill ?? '',
|
|
1371
|
+
style.numberFormat ?? ''
|
|
1372
|
+
];
|
|
1373
|
+
// Border properties
|
|
1374
|
+
if (style.border) {
|
|
1375
|
+
parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');
|
|
1376
|
+
} else {
|
|
1377
|
+
parts.push('', '', '', '');
|
|
1378
|
+
}
|
|
1379
|
+
// Alignment properties
|
|
1380
|
+
if (style.alignment) {
|
|
1381
|
+
parts.push(style.alignment.horizontal ?? '', style.alignment.vertical ?? '', style.alignment.wrapText ? '1' : '0', style.alignment.textRotation?.toString() ?? '');
|
|
1382
|
+
} else {
|
|
1383
|
+
parts.push('', '', '0', '');
|
|
1384
|
+
}
|
|
1385
|
+
return parts.join(SEP);
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1043
1388
|
* Parse styles from XML content
|
|
1044
1389
|
*/ static parse(xml) {
|
|
1045
1390
|
const styles = new Styles();
|
|
@@ -1206,6 +1551,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1206
1551
|
/**
|
|
1207
1552
|
* Get a style by index
|
|
1208
1553
|
*/ getStyle(index) {
|
|
1554
|
+
const cached = this._styleObjectCache.get(index);
|
|
1555
|
+
if (cached) return {
|
|
1556
|
+
...cached
|
|
1557
|
+
};
|
|
1209
1558
|
const xf = this._cellXfs[index];
|
|
1210
1559
|
if (!xf) return {};
|
|
1211
1560
|
const font = this._fonts[xf.fontId];
|
|
@@ -1246,13 +1595,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1246
1595
|
textRotation: xf.alignment.textRotation
|
|
1247
1596
|
};
|
|
1248
1597
|
}
|
|
1598
|
+
this._styleObjectCache.set(index, {
|
|
1599
|
+
...style
|
|
1600
|
+
});
|
|
1249
1601
|
return style;
|
|
1250
1602
|
}
|
|
1251
1603
|
/**
|
|
1252
1604
|
* Create a style and return its index
|
|
1253
1605
|
* Uses caching to deduplicate identical styles
|
|
1254
1606
|
*/ createStyle(style) {
|
|
1255
|
-
const key =
|
|
1607
|
+
const key = this._getStyleKey(style);
|
|
1256
1608
|
const cached = this._styleCache.get(key);
|
|
1257
1609
|
if (cached !== undefined) {
|
|
1258
1610
|
return cached;
|
|
@@ -1284,8 +1636,20 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1284
1636
|
const index = this._cellXfs.length;
|
|
1285
1637
|
this._cellXfs.push(xf);
|
|
1286
1638
|
this._styleCache.set(key, index);
|
|
1639
|
+
this._styleObjectCache.set(index, {
|
|
1640
|
+
...style
|
|
1641
|
+
});
|
|
1287
1642
|
return index;
|
|
1288
1643
|
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Clone an existing style by index, optionally overriding fields.
|
|
1646
|
+
*/ cloneStyle(index, overrides = {}) {
|
|
1647
|
+
const baseStyle = this.getStyle(index);
|
|
1648
|
+
return this.createStyle({
|
|
1649
|
+
...baseStyle,
|
|
1650
|
+
...overrides
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1289
1653
|
_findOrCreateFont(style) {
|
|
1290
1654
|
const font = {
|
|
1291
1655
|
bold: style.bold || false,
|
|
@@ -1521,6 +1885,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1521
1885
|
this._dirty = false;
|
|
1522
1886
|
// Cache for style deduplication
|
|
1523
1887
|
this._styleCache = new Map();
|
|
1888
|
+
this._styleObjectCache = new Map();
|
|
1524
1889
|
}
|
|
1525
1890
|
}
|
|
1526
1891
|
|
|
@@ -1532,6 +1897,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1532
1897
|
this._columnFields = [];
|
|
1533
1898
|
this._valueFields = [];
|
|
1534
1899
|
this._filterFields = [];
|
|
1900
|
+
this._fieldAssignments = new Map();
|
|
1535
1901
|
this._name = name;
|
|
1536
1902
|
this._cache = cache;
|
|
1537
1903
|
this._targetSheet = targetSheet;
|
|
@@ -1573,11 +1939,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1573
1939
|
if (fieldIndex < 0) {
|
|
1574
1940
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1575
1941
|
}
|
|
1576
|
-
|
|
1942
|
+
const assignment = {
|
|
1577
1943
|
fieldName,
|
|
1578
1944
|
fieldIndex,
|
|
1579
1945
|
axis: 'row'
|
|
1580
|
-
}
|
|
1946
|
+
};
|
|
1947
|
+
this._rowFields.push(assignment);
|
|
1948
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1581
1949
|
return this;
|
|
1582
1950
|
}
|
|
1583
1951
|
/**
|
|
@@ -1588,11 +1956,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1588
1956
|
if (fieldIndex < 0) {
|
|
1589
1957
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1590
1958
|
}
|
|
1591
|
-
|
|
1959
|
+
const assignment = {
|
|
1592
1960
|
fieldName,
|
|
1593
1961
|
fieldIndex,
|
|
1594
1962
|
axis: 'column'
|
|
1595
|
-
}
|
|
1963
|
+
};
|
|
1964
|
+
this._columnFields.push(assignment);
|
|
1965
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1596
1966
|
return this;
|
|
1597
1967
|
}
|
|
1598
1968
|
/**
|
|
@@ -1606,13 +1976,15 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1606
1976
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1607
1977
|
}
|
|
1608
1978
|
const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
|
|
1609
|
-
|
|
1979
|
+
const assignment = {
|
|
1610
1980
|
fieldName,
|
|
1611
1981
|
fieldIndex,
|
|
1612
1982
|
axis: 'value',
|
|
1613
1983
|
aggregation,
|
|
1614
1984
|
displayName: displayName || defaultName
|
|
1615
|
-
}
|
|
1985
|
+
};
|
|
1986
|
+
this._valueFields.push(assignment);
|
|
1987
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1616
1988
|
return this;
|
|
1617
1989
|
}
|
|
1618
1990
|
/**
|
|
@@ -1623,11 +1995,44 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1623
1995
|
if (fieldIndex < 0) {
|
|
1624
1996
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1625
1997
|
}
|
|
1626
|
-
|
|
1998
|
+
const assignment = {
|
|
1627
1999
|
fieldName,
|
|
1628
2000
|
fieldIndex,
|
|
1629
2001
|
axis: 'filter'
|
|
1630
|
-
}
|
|
2002
|
+
};
|
|
2003
|
+
this._filterFields.push(assignment);
|
|
2004
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
2005
|
+
return this;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Set a sort order for a row/column field
|
|
2009
|
+
*/ sortField(fieldName, order) {
|
|
2010
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
2011
|
+
if (fieldIndex < 0) {
|
|
2012
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
2013
|
+
}
|
|
2014
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
2015
|
+
if (!assignment || assignment.axis !== 'row' && assignment.axis !== 'column') {
|
|
2016
|
+
throw new Error(`Field is not assigned to row or column axis: ${fieldName}`);
|
|
2017
|
+
}
|
|
2018
|
+
assignment.sortOrder = order;
|
|
2019
|
+
return this;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Filter items for a field (include or exclude list)
|
|
2023
|
+
*/ filterField(fieldName, filter) {
|
|
2024
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
2025
|
+
if (fieldIndex < 0) {
|
|
2026
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
2027
|
+
}
|
|
2028
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
2029
|
+
if (!assignment) {
|
|
2030
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
2031
|
+
}
|
|
2032
|
+
if (filter.include && filter.exclude) {
|
|
2033
|
+
throw new Error('Pivot field filter cannot use both include and exclude');
|
|
2034
|
+
}
|
|
2035
|
+
assignment.filter = filter;
|
|
1631
2036
|
return this;
|
|
1632
2037
|
}
|
|
1633
2038
|
/**
|
|
@@ -1792,17 +2197,28 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1792
2197
|
const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
1793
2198
|
const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
1794
2199
|
const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
2200
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
1795
2201
|
if (rowField) {
|
|
1796
2202
|
attrs.axis = 'axisRow';
|
|
1797
2203
|
attrs.showAll = '0';
|
|
2204
|
+
if (assignment?.sortOrder) {
|
|
2205
|
+
attrs.sortType = 'ascending';
|
|
2206
|
+
attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
2207
|
+
}
|
|
1798
2208
|
// Add items for shared values
|
|
1799
2209
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1800
2210
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1801
2211
|
const itemNodes = [];
|
|
2212
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1802
2213
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1803
|
-
|
|
2214
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2215
|
+
const itemAttrs = {
|
|
1804
2216
|
x: String(i)
|
|
1805
|
-
}
|
|
2217
|
+
};
|
|
2218
|
+
if (!shouldInclude) {
|
|
2219
|
+
itemAttrs.h = '1';
|
|
2220
|
+
}
|
|
2221
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1806
2222
|
}
|
|
1807
2223
|
// Add default subtotal item
|
|
1808
2224
|
itemNodes.push(createElement('item', {
|
|
@@ -1815,13 +2231,23 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1815
2231
|
} else if (colField) {
|
|
1816
2232
|
attrs.axis = 'axisCol';
|
|
1817
2233
|
attrs.showAll = '0';
|
|
2234
|
+
if (assignment?.sortOrder) {
|
|
2235
|
+
attrs.sortType = 'ascending';
|
|
2236
|
+
attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
2237
|
+
}
|
|
1818
2238
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1819
2239
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1820
2240
|
const itemNodes = [];
|
|
2241
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1821
2242
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1822
|
-
|
|
2243
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2244
|
+
const itemAttrs = {
|
|
1823
2245
|
x: String(i)
|
|
1824
|
-
}
|
|
2246
|
+
};
|
|
2247
|
+
if (!shouldInclude) {
|
|
2248
|
+
itemAttrs.h = '1';
|
|
2249
|
+
}
|
|
2250
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1825
2251
|
}
|
|
1826
2252
|
itemNodes.push(createElement('item', {
|
|
1827
2253
|
t: 'default'
|
|
@@ -1836,10 +2262,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1836
2262
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1837
2263
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1838
2264
|
const itemNodes = [];
|
|
2265
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1839
2266
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1840
|
-
|
|
2267
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2268
|
+
const itemAttrs = {
|
|
1841
2269
|
x: String(i)
|
|
1842
|
-
}
|
|
2270
|
+
};
|
|
2271
|
+
if (!shouldInclude) {
|
|
2272
|
+
itemAttrs.h = '1';
|
|
2273
|
+
}
|
|
2274
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1843
2275
|
}
|
|
1844
2276
|
itemNodes.push(createElement('item', {
|
|
1845
2277
|
t: 'default'
|
|
@@ -1856,6 +2288,31 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1856
2288
|
}
|
|
1857
2289
|
return createElement('pivotField', attrs, children);
|
|
1858
2290
|
}
|
|
2291
|
+
_resolveItemFilter(items, filter) {
|
|
2292
|
+
const allowed = new Set();
|
|
2293
|
+
if (!filter || !filter.include && !filter.exclude) {
|
|
2294
|
+
for(let i = 0; i < items.length; i++){
|
|
2295
|
+
allowed.add(i);
|
|
2296
|
+
}
|
|
2297
|
+
return allowed;
|
|
2298
|
+
}
|
|
2299
|
+
if (filter.include) {
|
|
2300
|
+
for(let i = 0; i < items.length; i++){
|
|
2301
|
+
if (filter.include.includes(items[i])) {
|
|
2302
|
+
allowed.add(i);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return allowed;
|
|
2306
|
+
}
|
|
2307
|
+
if (filter.exclude) {
|
|
2308
|
+
for(let i = 0; i < items.length; i++){
|
|
2309
|
+
if (!filter.exclude.includes(items[i])) {
|
|
2310
|
+
allowed.add(i);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return allowed;
|
|
2315
|
+
}
|
|
1859
2316
|
/**
|
|
1860
2317
|
* Build row items based on unique values in row fields
|
|
1861
2318
|
*/ _buildRowItems() {
|
|
@@ -2006,6 +2463,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2006
2463
|
this._records = [];
|
|
2007
2464
|
this._recordCount = 0;
|
|
2008
2465
|
this._refreshOnLoad = true; // Default to true
|
|
2466
|
+
this._dateGrouping = false;
|
|
2009
2467
|
this._cacheId = cacheId;
|
|
2010
2468
|
this._sourceSheet = sourceSheet;
|
|
2011
2469
|
this._sourceRange = sourceRange;
|
|
@@ -2094,6 +2552,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2094
2552
|
}
|
|
2095
2553
|
}
|
|
2096
2554
|
}
|
|
2555
|
+
// Enable date grouping flag if any date field exists
|
|
2556
|
+
this._dateGrouping = this._fields.some((field)=>field.isDate);
|
|
2097
2557
|
// Store records
|
|
2098
2558
|
this._records = data;
|
|
2099
2559
|
}
|
|
@@ -2122,6 +2582,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2122
2582
|
v: item
|
|
2123
2583
|
}, []));
|
|
2124
2584
|
}
|
|
2585
|
+
} else if (field.isDate) {
|
|
2586
|
+
sharedItemsAttrs.containsDate = '1';
|
|
2125
2587
|
} else if (field.isNumeric) {
|
|
2126
2588
|
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
2127
2589
|
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
@@ -2152,9 +2614,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2152
2614
|
ref: this._sourceRange,
|
|
2153
2615
|
sheet: this._sourceSheet
|
|
2154
2616
|
}, []);
|
|
2155
|
-
const
|
|
2617
|
+
const cacheSourceAttrs = {
|
|
2156
2618
|
type: 'worksheet'
|
|
2157
|
-
}
|
|
2619
|
+
};
|
|
2620
|
+
if (this._dateGrouping) {
|
|
2621
|
+
cacheSourceAttrs.grouping = '1';
|
|
2622
|
+
}
|
|
2623
|
+
const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
|
|
2158
2624
|
worksheetSourceNode
|
|
2159
2625
|
]);
|
|
2160
2626
|
// Build attributes - refreshOnLoad should come early per OOXML schema
|
|
@@ -2300,6 +2766,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2300
2766
|
this._pivotTables = [];
|
|
2301
2767
|
this._pivotCaches = [];
|
|
2302
2768
|
this._nextCacheId = 0;
|
|
2769
|
+
// Date serialization handling
|
|
2770
|
+
this._dateHandling = 'jsDate';
|
|
2303
2771
|
this._sharedStrings = new SharedStrings();
|
|
2304
2772
|
this._styles = Styles.createDefault();
|
|
2305
2773
|
}
|
|
@@ -2364,6 +2832,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2364
2832
|
return this._styles;
|
|
2365
2833
|
}
|
|
2366
2834
|
/**
|
|
2835
|
+
* Get the workbook date handling strategy.
|
|
2836
|
+
*/ get dateHandling() {
|
|
2837
|
+
return this._dateHandling;
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Set the workbook date handling strategy.
|
|
2841
|
+
*/ set dateHandling(value) {
|
|
2842
|
+
this._dateHandling = value;
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2367
2845
|
* Get a worksheet by name or index
|
|
2368
2846
|
*/ sheet(nameOrIndex) {
|
|
2369
2847
|
let def;
|
|
@@ -3051,6 +3529,10 @@ exports.SharedStrings = SharedStrings;
|
|
|
3051
3529
|
exports.Styles = Styles;
|
|
3052
3530
|
exports.Workbook = Workbook;
|
|
3053
3531
|
exports.Worksheet = Worksheet;
|
|
3532
|
+
exports.colToLetter = colToLetter;
|
|
3533
|
+
exports.isInRange = isInRange;
|
|
3534
|
+
exports.letterToCol = letterToCol;
|
|
3535
|
+
exports.normalizeRange = normalizeRange;
|
|
3054
3536
|
exports.parseAddress = parseAddress;
|
|
3055
3537
|
exports.parseRange = parseRange;
|
|
3056
3538
|
exports.toAddress = toAddress;
|