@niicojs/excel 0.2.3 → 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 +232 -8
- package/dist/index.cjs +414 -28
- package/dist/index.d.cts +116 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +116 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -29
- package/package.json +1 -1
- package/src/index.ts +15 -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 +23 -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 +235 -7
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,65 @@ 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
|
+
/**
|
|
826
960
|
* Convert sheet data to an array of JSON objects.
|
|
827
961
|
*
|
|
828
962
|
* @param config - Configuration options
|
|
@@ -840,7 +974,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
840
974
|
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
841
975
|
* ```
|
|
842
976
|
*/ toJson(config = {}) {
|
|
843
|
-
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
|
|
977
|
+
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
|
|
844
978
|
// Get the bounds of data in the sheet
|
|
845
979
|
const bounds = this._getDataBounds();
|
|
846
980
|
if (!bounds) {
|
|
@@ -873,7 +1007,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
873
1007
|
for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
|
|
874
1008
|
const col = startCol + colOffset;
|
|
875
1009
|
const cell = this._cells.get(toAddress(row, col));
|
|
876
|
-
|
|
1010
|
+
let value = cell?.value ?? null;
|
|
1011
|
+
if (value instanceof Date) {
|
|
1012
|
+
value = this._serializeDate(value, dateHandling, cell);
|
|
1013
|
+
}
|
|
877
1014
|
if (value !== null) {
|
|
878
1015
|
hasData = true;
|
|
879
1016
|
}
|
|
@@ -890,10 +1027,24 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
890
1027
|
}
|
|
891
1028
|
return result;
|
|
892
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
|
+
}
|
|
893
1039
|
/**
|
|
894
1040
|
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
895
1041
|
*/ _getDataBounds() {
|
|
1042
|
+
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
1043
|
+
return this._dataBoundsCache;
|
|
1044
|
+
}
|
|
896
1045
|
if (this._cells.size === 0) {
|
|
1046
|
+
this._dataBoundsCache = null;
|
|
1047
|
+
this._boundsDirty = false;
|
|
897
1048
|
return null;
|
|
898
1049
|
}
|
|
899
1050
|
let minRow = Infinity;
|
|
@@ -909,14 +1060,18 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
909
1060
|
}
|
|
910
1061
|
}
|
|
911
1062
|
if (minRow === Infinity) {
|
|
1063
|
+
this._dataBoundsCache = null;
|
|
1064
|
+
this._boundsDirty = false;
|
|
912
1065
|
return null;
|
|
913
1066
|
}
|
|
914
|
-
|
|
1067
|
+
this._dataBoundsCache = {
|
|
915
1068
|
minRow,
|
|
916
1069
|
maxRow,
|
|
917
1070
|
minCol,
|
|
918
1071
|
maxCol
|
|
919
1072
|
};
|
|
1073
|
+
this._boundsDirty = false;
|
|
1074
|
+
return this._dataBoundsCache;
|
|
920
1075
|
}
|
|
921
1076
|
/**
|
|
922
1077
|
* Generate XML for this worksheet
|
|
@@ -930,6 +1085,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
930
1085
|
}
|
|
931
1086
|
rowMap.get(row).push(cell);
|
|
932
1087
|
}
|
|
1088
|
+
for (const rowIdx of this._rowHeights.keys()){
|
|
1089
|
+
if (!rowMap.has(rowIdx)) {
|
|
1090
|
+
rowMap.set(rowIdx, []);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
933
1093
|
// Sort rows and cells
|
|
934
1094
|
const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
|
|
935
1095
|
const rowNodes = [];
|
|
@@ -940,16 +1100,71 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
940
1100
|
const cellNode = this._buildCellNode(cell);
|
|
941
1101
|
cellNodes.push(cellNode);
|
|
942
1102
|
}
|
|
943
|
-
const
|
|
1103
|
+
const rowAttrs = {
|
|
944
1104
|
r: String(rowIdx + 1)
|
|
945
|
-
}
|
|
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);
|
|
946
1112
|
rowNodes.push(rowNode);
|
|
947
1113
|
}
|
|
948
1114
|
const sheetDataNode = createElement('sheetData', {}, rowNodes);
|
|
949
1115
|
// Build worksheet structure
|
|
950
|
-
const worksheetChildren = [
|
|
951
|
-
|
|
952
|
-
|
|
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);
|
|
953
1168
|
// Add merged cells if any
|
|
954
1169
|
if (this._mergedCells.size > 0) {
|
|
955
1170
|
const mergeCellNodes = [];
|
|
@@ -1136,6 +1351,40 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1136
1351
|
* Manages the styles (xl/styles.xml)
|
|
1137
1352
|
*/ class Styles {
|
|
1138
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
|
+
/**
|
|
1139
1388
|
* Parse styles from XML content
|
|
1140
1389
|
*/ static parse(xml) {
|
|
1141
1390
|
const styles = new Styles();
|
|
@@ -1302,6 +1551,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1302
1551
|
/**
|
|
1303
1552
|
* Get a style by index
|
|
1304
1553
|
*/ getStyle(index) {
|
|
1554
|
+
const cached = this._styleObjectCache.get(index);
|
|
1555
|
+
if (cached) return {
|
|
1556
|
+
...cached
|
|
1557
|
+
};
|
|
1305
1558
|
const xf = this._cellXfs[index];
|
|
1306
1559
|
if (!xf) return {};
|
|
1307
1560
|
const font = this._fonts[xf.fontId];
|
|
@@ -1342,13 +1595,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1342
1595
|
textRotation: xf.alignment.textRotation
|
|
1343
1596
|
};
|
|
1344
1597
|
}
|
|
1598
|
+
this._styleObjectCache.set(index, {
|
|
1599
|
+
...style
|
|
1600
|
+
});
|
|
1345
1601
|
return style;
|
|
1346
1602
|
}
|
|
1347
1603
|
/**
|
|
1348
1604
|
* Create a style and return its index
|
|
1349
1605
|
* Uses caching to deduplicate identical styles
|
|
1350
1606
|
*/ createStyle(style) {
|
|
1351
|
-
const key =
|
|
1607
|
+
const key = this._getStyleKey(style);
|
|
1352
1608
|
const cached = this._styleCache.get(key);
|
|
1353
1609
|
if (cached !== undefined) {
|
|
1354
1610
|
return cached;
|
|
@@ -1380,8 +1636,20 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1380
1636
|
const index = this._cellXfs.length;
|
|
1381
1637
|
this._cellXfs.push(xf);
|
|
1382
1638
|
this._styleCache.set(key, index);
|
|
1639
|
+
this._styleObjectCache.set(index, {
|
|
1640
|
+
...style
|
|
1641
|
+
});
|
|
1383
1642
|
return index;
|
|
1384
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
|
+
}
|
|
1385
1653
|
_findOrCreateFont(style) {
|
|
1386
1654
|
const font = {
|
|
1387
1655
|
bold: style.bold || false,
|
|
@@ -1617,6 +1885,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1617
1885
|
this._dirty = false;
|
|
1618
1886
|
// Cache for style deduplication
|
|
1619
1887
|
this._styleCache = new Map();
|
|
1888
|
+
this._styleObjectCache = new Map();
|
|
1620
1889
|
}
|
|
1621
1890
|
}
|
|
1622
1891
|
|
|
@@ -1628,6 +1897,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1628
1897
|
this._columnFields = [];
|
|
1629
1898
|
this._valueFields = [];
|
|
1630
1899
|
this._filterFields = [];
|
|
1900
|
+
this._fieldAssignments = new Map();
|
|
1631
1901
|
this._name = name;
|
|
1632
1902
|
this._cache = cache;
|
|
1633
1903
|
this._targetSheet = targetSheet;
|
|
@@ -1669,11 +1939,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1669
1939
|
if (fieldIndex < 0) {
|
|
1670
1940
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1671
1941
|
}
|
|
1672
|
-
|
|
1942
|
+
const assignment = {
|
|
1673
1943
|
fieldName,
|
|
1674
1944
|
fieldIndex,
|
|
1675
1945
|
axis: 'row'
|
|
1676
|
-
}
|
|
1946
|
+
};
|
|
1947
|
+
this._rowFields.push(assignment);
|
|
1948
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1677
1949
|
return this;
|
|
1678
1950
|
}
|
|
1679
1951
|
/**
|
|
@@ -1684,11 +1956,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1684
1956
|
if (fieldIndex < 0) {
|
|
1685
1957
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1686
1958
|
}
|
|
1687
|
-
|
|
1959
|
+
const assignment = {
|
|
1688
1960
|
fieldName,
|
|
1689
1961
|
fieldIndex,
|
|
1690
1962
|
axis: 'column'
|
|
1691
|
-
}
|
|
1963
|
+
};
|
|
1964
|
+
this._columnFields.push(assignment);
|
|
1965
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1692
1966
|
return this;
|
|
1693
1967
|
}
|
|
1694
1968
|
/**
|
|
@@ -1702,13 +1976,15 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1702
1976
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1703
1977
|
}
|
|
1704
1978
|
const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
|
|
1705
|
-
|
|
1979
|
+
const assignment = {
|
|
1706
1980
|
fieldName,
|
|
1707
1981
|
fieldIndex,
|
|
1708
1982
|
axis: 'value',
|
|
1709
1983
|
aggregation,
|
|
1710
1984
|
displayName: displayName || defaultName
|
|
1711
|
-
}
|
|
1985
|
+
};
|
|
1986
|
+
this._valueFields.push(assignment);
|
|
1987
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1712
1988
|
return this;
|
|
1713
1989
|
}
|
|
1714
1990
|
/**
|
|
@@ -1719,11 +1995,44 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1719
1995
|
if (fieldIndex < 0) {
|
|
1720
1996
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1721
1997
|
}
|
|
1722
|
-
|
|
1998
|
+
const assignment = {
|
|
1723
1999
|
fieldName,
|
|
1724
2000
|
fieldIndex,
|
|
1725
2001
|
axis: 'filter'
|
|
1726
|
-
}
|
|
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;
|
|
1727
2036
|
return this;
|
|
1728
2037
|
}
|
|
1729
2038
|
/**
|
|
@@ -1888,17 +2197,28 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1888
2197
|
const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
1889
2198
|
const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
1890
2199
|
const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
2200
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
1891
2201
|
if (rowField) {
|
|
1892
2202
|
attrs.axis = 'axisRow';
|
|
1893
2203
|
attrs.showAll = '0';
|
|
2204
|
+
if (assignment?.sortOrder) {
|
|
2205
|
+
attrs.sortType = 'ascending';
|
|
2206
|
+
attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
2207
|
+
}
|
|
1894
2208
|
// Add items for shared values
|
|
1895
2209
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1896
2210
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1897
2211
|
const itemNodes = [];
|
|
2212
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1898
2213
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1899
|
-
|
|
2214
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2215
|
+
const itemAttrs = {
|
|
1900
2216
|
x: String(i)
|
|
1901
|
-
}
|
|
2217
|
+
};
|
|
2218
|
+
if (!shouldInclude) {
|
|
2219
|
+
itemAttrs.h = '1';
|
|
2220
|
+
}
|
|
2221
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1902
2222
|
}
|
|
1903
2223
|
// Add default subtotal item
|
|
1904
2224
|
itemNodes.push(createElement('item', {
|
|
@@ -1911,13 +2231,23 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1911
2231
|
} else if (colField) {
|
|
1912
2232
|
attrs.axis = 'axisCol';
|
|
1913
2233
|
attrs.showAll = '0';
|
|
2234
|
+
if (assignment?.sortOrder) {
|
|
2235
|
+
attrs.sortType = 'ascending';
|
|
2236
|
+
attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
2237
|
+
}
|
|
1914
2238
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1915
2239
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1916
2240
|
const itemNodes = [];
|
|
2241
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1917
2242
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1918
|
-
|
|
2243
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2244
|
+
const itemAttrs = {
|
|
1919
2245
|
x: String(i)
|
|
1920
|
-
}
|
|
2246
|
+
};
|
|
2247
|
+
if (!shouldInclude) {
|
|
2248
|
+
itemAttrs.h = '1';
|
|
2249
|
+
}
|
|
2250
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1921
2251
|
}
|
|
1922
2252
|
itemNodes.push(createElement('item', {
|
|
1923
2253
|
t: 'default'
|
|
@@ -1932,10 +2262,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1932
2262
|
const cacheField = this._cache.fields[fieldIndex];
|
|
1933
2263
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
1934
2264
|
const itemNodes = [];
|
|
2265
|
+
const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
|
|
1935
2266
|
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
1936
|
-
|
|
2267
|
+
const shouldInclude = allowedIndexes.has(i);
|
|
2268
|
+
const itemAttrs = {
|
|
1937
2269
|
x: String(i)
|
|
1938
|
-
}
|
|
2270
|
+
};
|
|
2271
|
+
if (!shouldInclude) {
|
|
2272
|
+
itemAttrs.h = '1';
|
|
2273
|
+
}
|
|
2274
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
1939
2275
|
}
|
|
1940
2276
|
itemNodes.push(createElement('item', {
|
|
1941
2277
|
t: 'default'
|
|
@@ -1952,6 +2288,31 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
1952
2288
|
}
|
|
1953
2289
|
return createElement('pivotField', attrs, children);
|
|
1954
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
|
+
}
|
|
1955
2316
|
/**
|
|
1956
2317
|
* Build row items based on unique values in row fields
|
|
1957
2318
|
*/ _buildRowItems() {
|
|
@@ -2102,6 +2463,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2102
2463
|
this._records = [];
|
|
2103
2464
|
this._recordCount = 0;
|
|
2104
2465
|
this._refreshOnLoad = true; // Default to true
|
|
2466
|
+
this._dateGrouping = false;
|
|
2105
2467
|
this._cacheId = cacheId;
|
|
2106
2468
|
this._sourceSheet = sourceSheet;
|
|
2107
2469
|
this._sourceRange = sourceRange;
|
|
@@ -2190,6 +2552,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2190
2552
|
}
|
|
2191
2553
|
}
|
|
2192
2554
|
}
|
|
2555
|
+
// Enable date grouping flag if any date field exists
|
|
2556
|
+
this._dateGrouping = this._fields.some((field)=>field.isDate);
|
|
2193
2557
|
// Store records
|
|
2194
2558
|
this._records = data;
|
|
2195
2559
|
}
|
|
@@ -2218,6 +2582,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2218
2582
|
v: item
|
|
2219
2583
|
}, []));
|
|
2220
2584
|
}
|
|
2585
|
+
} else if (field.isDate) {
|
|
2586
|
+
sharedItemsAttrs.containsDate = '1';
|
|
2221
2587
|
} else if (field.isNumeric) {
|
|
2222
2588
|
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
2223
2589
|
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
@@ -2248,9 +2614,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2248
2614
|
ref: this._sourceRange,
|
|
2249
2615
|
sheet: this._sourceSheet
|
|
2250
2616
|
}, []);
|
|
2251
|
-
const
|
|
2617
|
+
const cacheSourceAttrs = {
|
|
2252
2618
|
type: 'worksheet'
|
|
2253
|
-
}
|
|
2619
|
+
};
|
|
2620
|
+
if (this._dateGrouping) {
|
|
2621
|
+
cacheSourceAttrs.grouping = '1';
|
|
2622
|
+
}
|
|
2623
|
+
const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
|
|
2254
2624
|
worksheetSourceNode
|
|
2255
2625
|
]);
|
|
2256
2626
|
// Build attributes - refreshOnLoad should come early per OOXML schema
|
|
@@ -2396,6 +2766,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2396
2766
|
this._pivotTables = [];
|
|
2397
2767
|
this._pivotCaches = [];
|
|
2398
2768
|
this._nextCacheId = 0;
|
|
2769
|
+
// Date serialization handling
|
|
2770
|
+
this._dateHandling = 'jsDate';
|
|
2399
2771
|
this._sharedStrings = new SharedStrings();
|
|
2400
2772
|
this._styles = Styles.createDefault();
|
|
2401
2773
|
}
|
|
@@ -2460,6 +2832,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
|
|
|
2460
2832
|
return this._styles;
|
|
2461
2833
|
}
|
|
2462
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
|
+
/**
|
|
2463
2845
|
* Get a worksheet by name or index
|
|
2464
2846
|
*/ sheet(nameOrIndex) {
|
|
2465
2847
|
let def;
|
|
@@ -3147,6 +3529,10 @@ exports.SharedStrings = SharedStrings;
|
|
|
3147
3529
|
exports.Styles = Styles;
|
|
3148
3530
|
exports.Workbook = Workbook;
|
|
3149
3531
|
exports.Worksheet = Worksheet;
|
|
3532
|
+
exports.colToLetter = colToLetter;
|
|
3533
|
+
exports.isInRange = isInRange;
|
|
3534
|
+
exports.letterToCol = letterToCol;
|
|
3535
|
+
exports.normalizeRange = normalizeRange;
|
|
3150
3536
|
exports.parseAddress = parseAddress;
|
|
3151
3537
|
exports.parseRange = parseRange;
|
|
3152
3538
|
exports.toAddress = toAddress;
|