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