@niicojs/excel 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +241 -8
- package/dist/index.cjs +1485 -167
- package/dist/index.d.cts +376 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +376 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1485 -168
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/pivot-cache.ts +10 -1
- package/src/pivot-table.ts +176 -40
- package/src/range.ts +15 -2
- package/src/shared-strings.ts +65 -16
- package/src/styles.ts +192 -21
- package/src/table.ts +386 -0
- package/src/types.ts +74 -2
- package/src/utils/address.ts +4 -1
- package/src/utils/xml.ts +0 -7
- package/src/workbook.ts +426 -41
- package/src/worksheet.ts +484 -27
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
|
|
@@ -433,12 +435,23 @@ _computedKey = Symbol.iterator;
|
|
|
433
435
|
/**
|
|
434
436
|
* Get all values in the range as a 2D array
|
|
435
437
|
*/ get values() {
|
|
438
|
+
return this.getValues();
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get all values in the range as a 2D array with options
|
|
442
|
+
*/ getValues(options = {}) {
|
|
443
|
+
const { createMissing = true } = options;
|
|
436
444
|
const result = [];
|
|
437
445
|
for(let r = this._range.start.row; r <= this._range.end.row; r++){
|
|
438
446
|
const row = [];
|
|
439
447
|
for(let c = this._range.start.col; c <= this._range.end.col; c++){
|
|
440
|
-
|
|
441
|
-
|
|
448
|
+
if (createMissing) {
|
|
449
|
+
const cell = this._worksheet.cell(r, c);
|
|
450
|
+
row.push(cell.value);
|
|
451
|
+
} else {
|
|
452
|
+
const cell = this._worksheet.getCellIfExists(r, c);
|
|
453
|
+
row.push(cell?.value ?? null);
|
|
454
|
+
}
|
|
442
455
|
}
|
|
443
456
|
result.push(row);
|
|
444
457
|
}
|
|
@@ -602,6 +615,322 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
602
615
|
};
|
|
603
616
|
};
|
|
604
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Maps table total function names to SUBTOTAL function numbers
|
|
620
|
+
* SUBTOTAL uses 101-111 for functions that ignore hidden values
|
|
621
|
+
*/ const TOTAL_FUNCTION_NUMBERS = {
|
|
622
|
+
average: 101,
|
|
623
|
+
count: 102,
|
|
624
|
+
countNums: 103,
|
|
625
|
+
max: 104,
|
|
626
|
+
min: 105,
|
|
627
|
+
stdDev: 107,
|
|
628
|
+
sum: 109,
|
|
629
|
+
var: 110,
|
|
630
|
+
none: 0
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Maps table total function names to XML attribute values
|
|
634
|
+
*/ const TOTAL_FUNCTION_NAMES = {
|
|
635
|
+
average: 'average',
|
|
636
|
+
count: 'count',
|
|
637
|
+
countNums: 'countNums',
|
|
638
|
+
max: 'max',
|
|
639
|
+
min: 'min',
|
|
640
|
+
stdDev: 'stdDev',
|
|
641
|
+
sum: 'sum',
|
|
642
|
+
var: 'var',
|
|
643
|
+
none: 'none'
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Represents an Excel Table (ListObject) with auto-filter, banded styling, and total row.
|
|
647
|
+
*/ class Table {
|
|
648
|
+
constructor(worksheet, config, tableId){
|
|
649
|
+
this._columns = [];
|
|
650
|
+
this._dirty = true;
|
|
651
|
+
this._worksheet = worksheet;
|
|
652
|
+
this._name = config.name;
|
|
653
|
+
this._displayName = config.name;
|
|
654
|
+
this._range = parseRange(config.range);
|
|
655
|
+
this._baseRange = {
|
|
656
|
+
start: {
|
|
657
|
+
...this._range.start
|
|
658
|
+
},
|
|
659
|
+
end: {
|
|
660
|
+
...this._range.end
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
this._totalRow = config.totalRow === true; // Default false
|
|
664
|
+
this._autoFilter = true; // Tables have auto-filter by default
|
|
665
|
+
this._headerRow = config.headerRow !== false;
|
|
666
|
+
this._id = tableId;
|
|
667
|
+
// Expand range to include total row if enabled
|
|
668
|
+
if (this._totalRow) {
|
|
669
|
+
this._range.end.row++;
|
|
670
|
+
}
|
|
671
|
+
// Set default style
|
|
672
|
+
this._style = {
|
|
673
|
+
name: config.style?.name ?? 'TableStyleMedium2',
|
|
674
|
+
showRowStripes: config.style?.showRowStripes !== false,
|
|
675
|
+
showColumnStripes: config.style?.showColumnStripes === true,
|
|
676
|
+
showFirstColumn: config.style?.showFirstColumn === true,
|
|
677
|
+
showLastColumn: config.style?.showLastColumn === true
|
|
678
|
+
};
|
|
679
|
+
// Extract column names from worksheet headers
|
|
680
|
+
this._extractColumns();
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get the table name
|
|
684
|
+
*/ get name() {
|
|
685
|
+
return this._name;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get the table display name
|
|
689
|
+
*/ get displayName() {
|
|
690
|
+
return this._displayName;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Get the table ID
|
|
694
|
+
*/ get id() {
|
|
695
|
+
return this._id;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Get the worksheet this table belongs to
|
|
699
|
+
*/ get worksheet() {
|
|
700
|
+
return this._worksheet;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Get the table range address string
|
|
704
|
+
*/ get range() {
|
|
705
|
+
return toRange(this._range);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Get the base range excluding total row
|
|
709
|
+
*/ get baseRange() {
|
|
710
|
+
return toRange(this._baseRange);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Get the table range as RangeAddress
|
|
714
|
+
*/ get rangeAddress() {
|
|
715
|
+
return {
|
|
716
|
+
...this._range
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Get column names
|
|
721
|
+
*/ get columns() {
|
|
722
|
+
return this._columns.map((c)=>c.name);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Check if table has a total row
|
|
726
|
+
*/ get hasTotalRow() {
|
|
727
|
+
return this._totalRow;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Check if table has a header row
|
|
731
|
+
*/ get hasHeaderRow() {
|
|
732
|
+
return this._headerRow;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Check if table has auto-filter enabled
|
|
736
|
+
*/ get hasAutoFilter() {
|
|
737
|
+
return this._autoFilter;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Get the current style configuration
|
|
741
|
+
*/ get style() {
|
|
742
|
+
return {
|
|
743
|
+
...this._style
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Check if the table has been modified
|
|
748
|
+
*/ get dirty() {
|
|
749
|
+
return this._dirty;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Set a total function for a column
|
|
753
|
+
* @param columnName - Name of the column (header text)
|
|
754
|
+
* @param fn - Aggregation function to use
|
|
755
|
+
* @returns this for method chaining
|
|
756
|
+
*/ setTotalFunction(columnName, fn) {
|
|
757
|
+
if (!this._totalRow) {
|
|
758
|
+
throw new Error('Cannot set total function: table does not have a total row enabled');
|
|
759
|
+
}
|
|
760
|
+
const column = this._columns.find((c)=>c.name === columnName);
|
|
761
|
+
if (!column) {
|
|
762
|
+
throw new Error(`Column not found: ${columnName}`);
|
|
763
|
+
}
|
|
764
|
+
column.totalFunction = fn;
|
|
765
|
+
this._dirty = true;
|
|
766
|
+
// Write the formula to the total row cell
|
|
767
|
+
this._writeTotalRowFormula(column);
|
|
768
|
+
return this;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Get total function for a column if set
|
|
772
|
+
*/ getTotalFunction(columnName) {
|
|
773
|
+
const column = this._columns.find((c)=>c.name === columnName);
|
|
774
|
+
return column?.totalFunction;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Enable or disable auto-filter
|
|
778
|
+
* @param enabled - Whether auto-filter should be enabled
|
|
779
|
+
* @returns this for method chaining
|
|
780
|
+
*/ setAutoFilter(enabled) {
|
|
781
|
+
this._autoFilter = enabled;
|
|
782
|
+
this._dirty = true;
|
|
783
|
+
return this;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Update table style configuration
|
|
787
|
+
* @param style - Style options to apply
|
|
788
|
+
* @returns this for method chaining
|
|
789
|
+
*/ setStyle(style) {
|
|
790
|
+
if (style.name !== undefined) this._style.name = style.name;
|
|
791
|
+
if (style.showRowStripes !== undefined) this._style.showRowStripes = style.showRowStripes;
|
|
792
|
+
if (style.showColumnStripes !== undefined) this._style.showColumnStripes = style.showColumnStripes;
|
|
793
|
+
if (style.showFirstColumn !== undefined) this._style.showFirstColumn = style.showFirstColumn;
|
|
794
|
+
if (style.showLastColumn !== undefined) this._style.showLastColumn = style.showLastColumn;
|
|
795
|
+
this._dirty = true;
|
|
796
|
+
return this;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Enable or disable the total row
|
|
800
|
+
* @param enabled - Whether total row should be shown
|
|
801
|
+
* @returns this for method chaining
|
|
802
|
+
*/ setTotalRow(enabled) {
|
|
803
|
+
if (this._totalRow === enabled) return this;
|
|
804
|
+
this._totalRow = enabled;
|
|
805
|
+
this._dirty = true;
|
|
806
|
+
if (enabled) {
|
|
807
|
+
this._range = {
|
|
808
|
+
start: {
|
|
809
|
+
...this._baseRange.start
|
|
810
|
+
},
|
|
811
|
+
end: {
|
|
812
|
+
...this._baseRange.end
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
this._range.end.row++;
|
|
816
|
+
} else {
|
|
817
|
+
this._range = {
|
|
818
|
+
start: {
|
|
819
|
+
...this._baseRange.start
|
|
820
|
+
},
|
|
821
|
+
end: {
|
|
822
|
+
...this._baseRange.end
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
for (const col of this._columns){
|
|
826
|
+
col.totalFunction = undefined;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return this;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Extract column names from the header row of the worksheet
|
|
833
|
+
*/ _extractColumns() {
|
|
834
|
+
const headerRow = this._range.start.row;
|
|
835
|
+
const startCol = this._range.start.col;
|
|
836
|
+
const endCol = this._range.end.col;
|
|
837
|
+
for(let col = startCol; col <= endCol; col++){
|
|
838
|
+
const cell = this._headerRow ? this._worksheet.getCellIfExists(headerRow, col) : undefined;
|
|
839
|
+
const value = cell?.value;
|
|
840
|
+
const name = value != null ? String(value) : `Column${col - startCol + 1}`;
|
|
841
|
+
this._columns.push({
|
|
842
|
+
id: col - startCol + 1,
|
|
843
|
+
name,
|
|
844
|
+
colIndex: col
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Write the SUBTOTAL formula to a total row cell
|
|
850
|
+
*/ _writeTotalRowFormula(column) {
|
|
851
|
+
if (!this._totalRow || !column.totalFunction || column.totalFunction === 'none') {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const totalRowIndex = this._range.end.row;
|
|
855
|
+
const cell = this._worksheet.cell(totalRowIndex, column.colIndex);
|
|
856
|
+
// Generate SUBTOTAL formula with structured reference
|
|
857
|
+
const funcNum = TOTAL_FUNCTION_NUMBERS[column.totalFunction];
|
|
858
|
+
// Use structured reference: SUBTOTAL(109,[ColumnName])
|
|
859
|
+
const formula = `SUBTOTAL(${funcNum},[${column.name}])`;
|
|
860
|
+
cell.formula = formula;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Get the auto-filter range (excludes total row if present)
|
|
864
|
+
*/ _getAutoFilterRange() {
|
|
865
|
+
const start = toAddress(this._range.start.row, this._range.start.col);
|
|
866
|
+
// Auto-filter excludes the total row
|
|
867
|
+
let endRow = this._range.end.row;
|
|
868
|
+
if (this._totalRow) {
|
|
869
|
+
endRow--;
|
|
870
|
+
}
|
|
871
|
+
const end = toAddress(endRow, this._range.end.col);
|
|
872
|
+
return `${start}:${end}`;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Generate the table definition XML
|
|
876
|
+
*/ toXml() {
|
|
877
|
+
const children = [];
|
|
878
|
+
// Auto-filter element
|
|
879
|
+
if (this._autoFilter) {
|
|
880
|
+
const autoFilterRef = this._getAutoFilterRange();
|
|
881
|
+
children.push(createElement('autoFilter', {
|
|
882
|
+
ref: autoFilterRef
|
|
883
|
+
}, []));
|
|
884
|
+
}
|
|
885
|
+
// Table columns
|
|
886
|
+
const columnNodes = this._columns.map((col)=>{
|
|
887
|
+
const attrs = {
|
|
888
|
+
id: String(col.id),
|
|
889
|
+
name: col.name
|
|
890
|
+
};
|
|
891
|
+
// Add total function if specified
|
|
892
|
+
if (this._totalRow && col.totalFunction && col.totalFunction !== 'none') {
|
|
893
|
+
attrs.totalsRowFunction = TOTAL_FUNCTION_NAMES[col.totalFunction];
|
|
894
|
+
}
|
|
895
|
+
return createElement('tableColumn', attrs, []);
|
|
896
|
+
});
|
|
897
|
+
children.push(createElement('tableColumns', {
|
|
898
|
+
count: String(columnNodes.length)
|
|
899
|
+
}, columnNodes));
|
|
900
|
+
// Table style info
|
|
901
|
+
const styleAttrs = {
|
|
902
|
+
name: this._style.name || 'TableStyleMedium2',
|
|
903
|
+
showFirstColumn: this._style.showFirstColumn ? '1' : '0',
|
|
904
|
+
showLastColumn: this._style.showLastColumn ? '1' : '0',
|
|
905
|
+
showRowStripes: this._style.showRowStripes !== false ? '1' : '0',
|
|
906
|
+
showColumnStripes: this._style.showColumnStripes ? '1' : '0'
|
|
907
|
+
};
|
|
908
|
+
children.push(createElement('tableStyleInfo', styleAttrs, []));
|
|
909
|
+
// Build table attributes
|
|
910
|
+
const tableRef = toRange(this._range);
|
|
911
|
+
const tableAttrs = {
|
|
912
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
913
|
+
id: String(this._id),
|
|
914
|
+
name: this._name,
|
|
915
|
+
displayName: this._displayName,
|
|
916
|
+
ref: tableRef
|
|
917
|
+
};
|
|
918
|
+
if (!this._headerRow) {
|
|
919
|
+
tableAttrs.headerRowCount = '0';
|
|
920
|
+
}
|
|
921
|
+
if (this._totalRow) {
|
|
922
|
+
tableAttrs.totalsRowCount = '1';
|
|
923
|
+
} else {
|
|
924
|
+
tableAttrs.totalsRowShown = '0';
|
|
925
|
+
}
|
|
926
|
+
// Build complete table node
|
|
927
|
+
const tableNode = createElement('table', tableAttrs, children);
|
|
928
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
929
|
+
tableNode
|
|
930
|
+
])}`;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
605
934
|
/**
|
|
606
935
|
* Represents a worksheet in a workbook
|
|
607
936
|
*/ class Worksheet {
|
|
@@ -611,6 +940,17 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
611
940
|
this._dirty = false;
|
|
612
941
|
this._mergedCells = new Set();
|
|
613
942
|
this._sheetData = [];
|
|
943
|
+
this._columnWidths = new Map();
|
|
944
|
+
this._rowHeights = new Map();
|
|
945
|
+
this._frozenPane = null;
|
|
946
|
+
this._dataBoundsCache = null;
|
|
947
|
+
this._boundsDirty = true;
|
|
948
|
+
this._tables = [];
|
|
949
|
+
this._preserveXml = false;
|
|
950
|
+
this._tableRelIds = null;
|
|
951
|
+
this._sheetViewsDirty = false;
|
|
952
|
+
this._colsDirty = false;
|
|
953
|
+
this._tablePartsDirty = false;
|
|
614
954
|
this._workbook = workbook;
|
|
615
955
|
this._name = name;
|
|
616
956
|
}
|
|
@@ -634,15 +974,53 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
634
974
|
* Parse worksheet XML content
|
|
635
975
|
*/ parse(xml) {
|
|
636
976
|
this._xmlNodes = parseXml(xml);
|
|
977
|
+
this._preserveXml = true;
|
|
637
978
|
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
638
979
|
if (!worksheet) return;
|
|
639
980
|
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
981
|
+
// Parse sheet views (freeze panes)
|
|
982
|
+
const sheetViews = findElement(worksheetChildren, 'sheetViews');
|
|
983
|
+
if (sheetViews) {
|
|
984
|
+
const viewChildren = getChildren(sheetViews, 'sheetViews');
|
|
985
|
+
const sheetView = findElement(viewChildren, 'sheetView');
|
|
986
|
+
if (sheetView) {
|
|
987
|
+
const sheetViewChildren = getChildren(sheetView, 'sheetView');
|
|
988
|
+
const pane = findElement(sheetViewChildren, 'pane');
|
|
989
|
+
if (pane && getAttr(pane, 'state') === 'frozen') {
|
|
990
|
+
const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);
|
|
991
|
+
const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);
|
|
992
|
+
if (xSplit > 0 || ySplit > 0) {
|
|
993
|
+
this._frozenPane = {
|
|
994
|
+
row: ySplit,
|
|
995
|
+
col: xSplit
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
640
1001
|
// Parse sheet data (cells)
|
|
641
1002
|
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
642
1003
|
if (sheetData) {
|
|
643
1004
|
this._sheetData = getChildren(sheetData, 'sheetData');
|
|
644
1005
|
this._parseSheetData(this._sheetData);
|
|
645
1006
|
}
|
|
1007
|
+
// Parse column widths
|
|
1008
|
+
const cols = findElement(worksheetChildren, 'cols');
|
|
1009
|
+
if (cols) {
|
|
1010
|
+
const colChildren = getChildren(cols, 'cols');
|
|
1011
|
+
for (const col of colChildren){
|
|
1012
|
+
if (!('col' in col)) continue;
|
|
1013
|
+
const min = parseInt(getAttr(col, 'min') || '0', 10);
|
|
1014
|
+
const max = parseInt(getAttr(col, 'max') || '0', 10);
|
|
1015
|
+
const width = parseFloat(getAttr(col, 'width') || '0');
|
|
1016
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
1017
|
+
if (min > 0 && max > 0) {
|
|
1018
|
+
for(let idx = min; idx <= max; idx++){
|
|
1019
|
+
this._columnWidths.set(idx - 1, width);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
646
1024
|
// Parse merged cells
|
|
647
1025
|
const mergeCells = findElement(worksheetChildren, 'mergeCells');
|
|
648
1026
|
if (mergeCells) {
|
|
@@ -662,6 +1040,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
662
1040
|
*/ _parseSheetData(rows) {
|
|
663
1041
|
for (const rowNode of rows){
|
|
664
1042
|
if (!('row' in rowNode)) continue;
|
|
1043
|
+
const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;
|
|
1044
|
+
const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');
|
|
1045
|
+
if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {
|
|
1046
|
+
this._rowHeights.set(rowIndex, rowHeight);
|
|
1047
|
+
}
|
|
665
1048
|
const rowChildren = getChildren(rowNode, 'row');
|
|
666
1049
|
for (const cellNode of rowChildren){
|
|
667
1050
|
if (!('c' in cellNode)) continue;
|
|
@@ -673,6 +1056,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
673
1056
|
this._cells.set(ref, cell);
|
|
674
1057
|
}
|
|
675
1058
|
}
|
|
1059
|
+
this._boundsDirty = true;
|
|
676
1060
|
}
|
|
677
1061
|
/**
|
|
678
1062
|
* Parse a cell XML node to CellData
|
|
@@ -759,9 +1143,17 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
759
1143
|
if (!cell) {
|
|
760
1144
|
cell = new Cell(this, row, c);
|
|
761
1145
|
this._cells.set(address, cell);
|
|
1146
|
+
this._boundsDirty = true;
|
|
762
1147
|
}
|
|
763
1148
|
return cell;
|
|
764
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Get an existing cell without creating it.
|
|
1152
|
+
*/ getCellIfExists(rowOrAddress, col) {
|
|
1153
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
1154
|
+
const address = toAddress(row, c);
|
|
1155
|
+
return this._cells.get(address);
|
|
1156
|
+
}
|
|
765
1157
|
range(startRowOrRange, startCol, endRow, endCol) {
|
|
766
1158
|
let rangeAddr;
|
|
767
1159
|
if (typeof startRowOrRange === 'string') {
|
|
@@ -821,6 +1213,145 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
821
1213
|
return this._cells;
|
|
822
1214
|
}
|
|
823
1215
|
/**
|
|
1216
|
+
* Set a column width (0-based index or column letter)
|
|
1217
|
+
*/ setColumnWidth(col, width) {
|
|
1218
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
1219
|
+
throw new Error('Column width must be a positive number');
|
|
1220
|
+
}
|
|
1221
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
1222
|
+
if (colIndex < 0) {
|
|
1223
|
+
throw new Error(`Invalid column: ${col}`);
|
|
1224
|
+
}
|
|
1225
|
+
this._columnWidths.set(colIndex, width);
|
|
1226
|
+
this._colsDirty = true;
|
|
1227
|
+
this._dirty = true;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Get a column width if set
|
|
1231
|
+
*/ getColumnWidth(col) {
|
|
1232
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
1233
|
+
return this._columnWidths.get(colIndex);
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Set a row height (0-based index)
|
|
1237
|
+
*/ setRowHeight(row, height) {
|
|
1238
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
1239
|
+
throw new Error('Row height must be a positive number');
|
|
1240
|
+
}
|
|
1241
|
+
if (row < 0) {
|
|
1242
|
+
throw new Error('Row index must be >= 0');
|
|
1243
|
+
}
|
|
1244
|
+
this._rowHeights.set(row, height);
|
|
1245
|
+
this._colsDirty = true;
|
|
1246
|
+
this._dirty = true;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Get a row height if set
|
|
1250
|
+
*/ getRowHeight(row) {
|
|
1251
|
+
return this._rowHeights.get(row);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Freeze panes at a given row/column split (counts from top-left)
|
|
1255
|
+
*/ freezePane(rowSplit, colSplit) {
|
|
1256
|
+
if (rowSplit < 0 || colSplit < 0) {
|
|
1257
|
+
throw new Error('Freeze pane splits must be >= 0');
|
|
1258
|
+
}
|
|
1259
|
+
if (rowSplit === 0 && colSplit === 0) {
|
|
1260
|
+
this._frozenPane = null;
|
|
1261
|
+
} else {
|
|
1262
|
+
this._frozenPane = {
|
|
1263
|
+
row: rowSplit,
|
|
1264
|
+
col: colSplit
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
this._sheetViewsDirty = true;
|
|
1268
|
+
this._dirty = true;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Get current frozen pane configuration
|
|
1272
|
+
*/ getFrozenPane() {
|
|
1273
|
+
return this._frozenPane ? {
|
|
1274
|
+
...this._frozenPane
|
|
1275
|
+
} : null;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Get all tables in the worksheet
|
|
1279
|
+
*/ get tables() {
|
|
1280
|
+
return [
|
|
1281
|
+
...this._tables
|
|
1282
|
+
];
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Get column width entries
|
|
1286
|
+
* @internal
|
|
1287
|
+
*/ getColumnWidths() {
|
|
1288
|
+
return new Map(this._columnWidths);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Get row height entries
|
|
1292
|
+
* @internal
|
|
1293
|
+
*/ getRowHeights() {
|
|
1294
|
+
return new Map(this._rowHeights);
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Set table relationship IDs for tableParts generation.
|
|
1298
|
+
* @internal
|
|
1299
|
+
*/ setTableRelIds(ids) {
|
|
1300
|
+
this._tableRelIds = ids ? [
|
|
1301
|
+
...ids
|
|
1302
|
+
] : null;
|
|
1303
|
+
this._tablePartsDirty = true;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Create an Excel Table (ListObject) from a data range.
|
|
1307
|
+
*
|
|
1308
|
+
* Tables provide structured data features like auto-filter, banded styling,
|
|
1309
|
+
* and total row with aggregation functions.
|
|
1310
|
+
*
|
|
1311
|
+
* @param config - Table configuration
|
|
1312
|
+
* @returns Table instance for method chaining
|
|
1313
|
+
*
|
|
1314
|
+
* @example
|
|
1315
|
+
* ```typescript
|
|
1316
|
+
* // Create a table with default styling
|
|
1317
|
+
* const table = sheet.createTable({
|
|
1318
|
+
* name: 'SalesData',
|
|
1319
|
+
* range: 'A1:D10',
|
|
1320
|
+
* });
|
|
1321
|
+
*
|
|
1322
|
+
* // Create a table with total row
|
|
1323
|
+
* const table = sheet.createTable({
|
|
1324
|
+
* name: 'SalesData',
|
|
1325
|
+
* range: 'A1:D10',
|
|
1326
|
+
* totalRow: true,
|
|
1327
|
+
* style: { name: 'TableStyleMedium2' }
|
|
1328
|
+
* });
|
|
1329
|
+
*
|
|
1330
|
+
* table.setTotalFunction('Sales', 'sum');
|
|
1331
|
+
* ```
|
|
1332
|
+
*/ createTable(config) {
|
|
1333
|
+
// Validate table name is unique within the workbook
|
|
1334
|
+
for (const sheet of this._workbook.sheetNames){
|
|
1335
|
+
const ws = this._workbook.sheet(sheet);
|
|
1336
|
+
for (const table of ws._tables){
|
|
1337
|
+
if (table.name === config.name) {
|
|
1338
|
+
throw new Error(`Table name already exists: ${config.name}`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
// Validate table name format (Excel rules: no spaces at start/end, alphanumeric + underscore)
|
|
1343
|
+
if (!config.name || !/^[A-Za-z_\\][A-Za-z0-9_.\\]*$/.test(config.name)) {
|
|
1344
|
+
throw new Error(`Invalid table name: ${config.name}. Names must start with a letter or underscore and contain only alphanumeric characters, underscores, or periods.`);
|
|
1345
|
+
}
|
|
1346
|
+
// Create the table with a unique ID from the workbook
|
|
1347
|
+
const tableId = this._workbook.getNextTableId();
|
|
1348
|
+
const table = new Table(this, config, tableId);
|
|
1349
|
+
this._tables.push(table);
|
|
1350
|
+
this._tablePartsDirty = true;
|
|
1351
|
+
this._dirty = true;
|
|
1352
|
+
return table;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
824
1355
|
* Convert sheet data to an array of JSON objects.
|
|
825
1356
|
*
|
|
826
1357
|
* @param config - Configuration options
|
|
@@ -838,7 +1369,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
838
1369
|
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
839
1370
|
* ```
|
|
840
1371
|
*/ toJson(config = {}) {
|
|
841
|
-
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
|
|
1372
|
+
const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
|
|
842
1373
|
// Get the bounds of data in the sheet
|
|
843
1374
|
const bounds = this._getDataBounds();
|
|
844
1375
|
if (!bounds) {
|
|
@@ -871,7 +1402,10 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
871
1402
|
for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
|
|
872
1403
|
const col = startCol + colOffset;
|
|
873
1404
|
const cell = this._cells.get(toAddress(row, col));
|
|
874
|
-
|
|
1405
|
+
let value = cell?.value ?? null;
|
|
1406
|
+
if (value instanceof Date) {
|
|
1407
|
+
value = this._serializeDate(value, dateHandling, cell);
|
|
1408
|
+
}
|
|
875
1409
|
if (value !== null) {
|
|
876
1410
|
hasData = true;
|
|
877
1411
|
}
|
|
@@ -888,10 +1422,24 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
888
1422
|
}
|
|
889
1423
|
return result;
|
|
890
1424
|
}
|
|
1425
|
+
_serializeDate(value, dateHandling, cell) {
|
|
1426
|
+
if (dateHandling === 'excelSerial') {
|
|
1427
|
+
return cell?._jsDateToExcel(value) ?? value;
|
|
1428
|
+
}
|
|
1429
|
+
if (dateHandling === 'isoString') {
|
|
1430
|
+
return value.toISOString();
|
|
1431
|
+
}
|
|
1432
|
+
return value;
|
|
1433
|
+
}
|
|
891
1434
|
/**
|
|
892
1435
|
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
893
1436
|
*/ _getDataBounds() {
|
|
1437
|
+
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
1438
|
+
return this._dataBoundsCache;
|
|
1439
|
+
}
|
|
894
1440
|
if (this._cells.size === 0) {
|
|
1441
|
+
this._dataBoundsCache = null;
|
|
1442
|
+
this._boundsDirty = false;
|
|
895
1443
|
return null;
|
|
896
1444
|
}
|
|
897
1445
|
let minRow = Infinity;
|
|
@@ -907,47 +1455,78 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
907
1455
|
}
|
|
908
1456
|
}
|
|
909
1457
|
if (minRow === Infinity) {
|
|
1458
|
+
this._dataBoundsCache = null;
|
|
1459
|
+
this._boundsDirty = false;
|
|
910
1460
|
return null;
|
|
911
1461
|
}
|
|
912
|
-
|
|
1462
|
+
this._dataBoundsCache = {
|
|
913
1463
|
minRow,
|
|
914
1464
|
maxRow,
|
|
915
1465
|
minCol,
|
|
916
1466
|
maxCol
|
|
917
1467
|
};
|
|
1468
|
+
this._boundsDirty = false;
|
|
1469
|
+
return this._dataBoundsCache;
|
|
918
1470
|
}
|
|
919
1471
|
/**
|
|
920
1472
|
* Generate XML for this worksheet
|
|
921
1473
|
*/ toXml() {
|
|
1474
|
+
const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;
|
|
922
1475
|
// Build sheetData from cells
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1476
|
+
const sheetDataNode = this._buildSheetDataNode();
|
|
1477
|
+
// Build worksheet structure
|
|
1478
|
+
const worksheetChildren = [];
|
|
1479
|
+
// Sheet views (freeze panes)
|
|
1480
|
+
if (this._frozenPane) {
|
|
1481
|
+
const paneAttrs = {
|
|
1482
|
+
state: 'frozen'
|
|
1483
|
+
};
|
|
1484
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
1485
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
1486
|
+
if (this._frozenPane.col > 0) {
|
|
1487
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
928
1488
|
}
|
|
929
|
-
|
|
1489
|
+
if (this._frozenPane.row > 0) {
|
|
1490
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
1491
|
+
}
|
|
1492
|
+
let activePane = 'bottomRight';
|
|
1493
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
1494
|
+
activePane = 'bottomLeft';
|
|
1495
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
1496
|
+
activePane = 'topRight';
|
|
1497
|
+
}
|
|
1498
|
+
paneAttrs.activePane = activePane;
|
|
1499
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
1500
|
+
const selectionNode = createElement('selection', {
|
|
1501
|
+
pane: activePane,
|
|
1502
|
+
activeCell: topLeftCell,
|
|
1503
|
+
sqref: topLeftCell
|
|
1504
|
+
}, []);
|
|
1505
|
+
const sheetViewNode = createElement('sheetView', {
|
|
1506
|
+
workbookViewId: '0'
|
|
1507
|
+
}, [
|
|
1508
|
+
paneNode,
|
|
1509
|
+
selectionNode
|
|
1510
|
+
]);
|
|
1511
|
+
worksheetChildren.push(createElement('sheetViews', {}, [
|
|
1512
|
+
sheetViewNode
|
|
1513
|
+
]));
|
|
930
1514
|
}
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1515
|
+
// Column widths
|
|
1516
|
+
if (this._columnWidths.size > 0) {
|
|
1517
|
+
const colNodes = [];
|
|
1518
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b)=>a[0] - b[0]);
|
|
1519
|
+
for (const [colIndex, width] of entries){
|
|
1520
|
+
colNodes.push(createElement('col', {
|
|
1521
|
+
min: String(colIndex + 1),
|
|
1522
|
+
max: String(colIndex + 1),
|
|
1523
|
+
width: String(width),
|
|
1524
|
+
customWidth: '1'
|
|
1525
|
+
}, []));
|
|
940
1526
|
}
|
|
941
|
-
|
|
942
|
-
r: String(rowIdx + 1)
|
|
943
|
-
}, cellNodes);
|
|
944
|
-
rowNodes.push(rowNode);
|
|
1527
|
+
worksheetChildren.push(createElement('cols', {}, colNodes));
|
|
945
1528
|
}
|
|
946
|
-
|
|
947
|
-
// Build worksheet structure
|
|
948
|
-
const worksheetChildren = [
|
|
949
|
-
sheetDataNode
|
|
950
|
-
];
|
|
1529
|
+
worksheetChildren.push(sheetDataNode);
|
|
951
1530
|
// Add merged cells if any
|
|
952
1531
|
if (this._mergedCells.size > 0) {
|
|
953
1532
|
const mergeCellNodes = [];
|
|
@@ -961,14 +1540,170 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
961
1540
|
}, mergeCellNodes);
|
|
962
1541
|
worksheetChildren.push(mergeCellsNode);
|
|
963
1542
|
}
|
|
1543
|
+
// Add table parts if any tables exist
|
|
1544
|
+
const tablePartsNode = this._buildTablePartsNode();
|
|
1545
|
+
if (tablePartsNode) {
|
|
1546
|
+
worksheetChildren.push(tablePartsNode);
|
|
1547
|
+
}
|
|
964
1548
|
const worksheetNode = createElement('worksheet', {
|
|
965
1549
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
966
1550
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
|
|
967
1551
|
}, worksheetChildren);
|
|
1552
|
+
if (preserved) {
|
|
1553
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
1554
|
+
preserved
|
|
1555
|
+
])}`;
|
|
1556
|
+
}
|
|
968
1557
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
969
1558
|
worksheetNode
|
|
970
1559
|
])}`;
|
|
971
1560
|
}
|
|
1561
|
+
_buildSheetDataNode() {
|
|
1562
|
+
const rowMap = new Map();
|
|
1563
|
+
for (const cell of this._cells.values()){
|
|
1564
|
+
const row = cell.row;
|
|
1565
|
+
if (!rowMap.has(row)) {
|
|
1566
|
+
rowMap.set(row, []);
|
|
1567
|
+
}
|
|
1568
|
+
rowMap.get(row).push(cell);
|
|
1569
|
+
}
|
|
1570
|
+
for (const rowIdx of this._rowHeights.keys()){
|
|
1571
|
+
if (!rowMap.has(rowIdx)) {
|
|
1572
|
+
rowMap.set(rowIdx, []);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
|
|
1576
|
+
const rowNodes = [];
|
|
1577
|
+
for (const [rowIdx, cells] of sortedRows){
|
|
1578
|
+
cells.sort((a, b)=>a.col - b.col);
|
|
1579
|
+
const cellNodes = [];
|
|
1580
|
+
for (const cell of cells){
|
|
1581
|
+
const cellNode = this._buildCellNode(cell);
|
|
1582
|
+
cellNodes.push(cellNode);
|
|
1583
|
+
}
|
|
1584
|
+
const rowAttrs = {
|
|
1585
|
+
r: String(rowIdx + 1)
|
|
1586
|
+
};
|
|
1587
|
+
const rowHeight = this._rowHeights.get(rowIdx);
|
|
1588
|
+
if (rowHeight !== undefined) {
|
|
1589
|
+
rowAttrs.ht = String(rowHeight);
|
|
1590
|
+
rowAttrs.customHeight = '1';
|
|
1591
|
+
}
|
|
1592
|
+
const rowNode = createElement('row', rowAttrs, cellNodes);
|
|
1593
|
+
rowNodes.push(rowNode);
|
|
1594
|
+
}
|
|
1595
|
+
return createElement('sheetData', {}, rowNodes);
|
|
1596
|
+
}
|
|
1597
|
+
_buildSheetViewsNode() {
|
|
1598
|
+
if (!this._frozenPane) return null;
|
|
1599
|
+
const paneAttrs = {
|
|
1600
|
+
state: 'frozen'
|
|
1601
|
+
};
|
|
1602
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
1603
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
1604
|
+
if (this._frozenPane.col > 0) {
|
|
1605
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
1606
|
+
}
|
|
1607
|
+
if (this._frozenPane.row > 0) {
|
|
1608
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
1609
|
+
}
|
|
1610
|
+
let activePane = 'bottomRight';
|
|
1611
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
1612
|
+
activePane = 'bottomLeft';
|
|
1613
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
1614
|
+
activePane = 'topRight';
|
|
1615
|
+
}
|
|
1616
|
+
paneAttrs.activePane = activePane;
|
|
1617
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
1618
|
+
const selectionNode = createElement('selection', {
|
|
1619
|
+
pane: activePane,
|
|
1620
|
+
activeCell: topLeftCell,
|
|
1621
|
+
sqref: topLeftCell
|
|
1622
|
+
}, []);
|
|
1623
|
+
const sheetViewNode = createElement('sheetView', {
|
|
1624
|
+
workbookViewId: '0'
|
|
1625
|
+
}, [
|
|
1626
|
+
paneNode,
|
|
1627
|
+
selectionNode
|
|
1628
|
+
]);
|
|
1629
|
+
return createElement('sheetViews', {}, [
|
|
1630
|
+
sheetViewNode
|
|
1631
|
+
]);
|
|
1632
|
+
}
|
|
1633
|
+
_buildColsNode() {
|
|
1634
|
+
if (this._columnWidths.size === 0) return null;
|
|
1635
|
+
const colNodes = [];
|
|
1636
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b)=>a[0] - b[0]);
|
|
1637
|
+
for (const [colIndex, width] of entries){
|
|
1638
|
+
colNodes.push(createElement('col', {
|
|
1639
|
+
min: String(colIndex + 1),
|
|
1640
|
+
max: String(colIndex + 1),
|
|
1641
|
+
width: String(width),
|
|
1642
|
+
customWidth: '1'
|
|
1643
|
+
}, []));
|
|
1644
|
+
}
|
|
1645
|
+
return createElement('cols', {}, colNodes);
|
|
1646
|
+
}
|
|
1647
|
+
_buildMergeCellsNode() {
|
|
1648
|
+
if (this._mergedCells.size === 0) return null;
|
|
1649
|
+
const mergeCellNodes = [];
|
|
1650
|
+
for (const ref of this._mergedCells){
|
|
1651
|
+
mergeCellNodes.push(createElement('mergeCell', {
|
|
1652
|
+
ref
|
|
1653
|
+
}, []));
|
|
1654
|
+
}
|
|
1655
|
+
return createElement('mergeCells', {
|
|
1656
|
+
count: String(this._mergedCells.size)
|
|
1657
|
+
}, mergeCellNodes);
|
|
1658
|
+
}
|
|
1659
|
+
_buildTablePartsNode() {
|
|
1660
|
+
if (this._tables.length === 0) return null;
|
|
1661
|
+
const tablePartNodes = [];
|
|
1662
|
+
for(let i = 0; i < this._tables.length; i++){
|
|
1663
|
+
const relId = this._tableRelIds && this._tableRelIds.length === this._tables.length ? this._tableRelIds[i] : `rId${i + 1}`;
|
|
1664
|
+
tablePartNodes.push(createElement('tablePart', {
|
|
1665
|
+
'r:id': relId
|
|
1666
|
+
}, []));
|
|
1667
|
+
}
|
|
1668
|
+
return createElement('tableParts', {
|
|
1669
|
+
count: String(this._tables.length)
|
|
1670
|
+
}, tablePartNodes);
|
|
1671
|
+
}
|
|
1672
|
+
_buildPreservedWorksheet() {
|
|
1673
|
+
if (!this._xmlNodes) return null;
|
|
1674
|
+
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
1675
|
+
if (!worksheet) return null;
|
|
1676
|
+
const children = getChildren(worksheet, 'worksheet');
|
|
1677
|
+
const upsertChild = (tag, node)=>{
|
|
1678
|
+
const existingIndex = children.findIndex((child)=>tag in child);
|
|
1679
|
+
if (node) {
|
|
1680
|
+
if (existingIndex >= 0) {
|
|
1681
|
+
children[existingIndex] = node;
|
|
1682
|
+
} else {
|
|
1683
|
+
children.push(node);
|
|
1684
|
+
}
|
|
1685
|
+
} else if (existingIndex >= 0) {
|
|
1686
|
+
children.splice(existingIndex, 1);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
if (this._sheetViewsDirty) {
|
|
1690
|
+
const sheetViewsNode = this._buildSheetViewsNode();
|
|
1691
|
+
upsertChild('sheetViews', sheetViewsNode);
|
|
1692
|
+
}
|
|
1693
|
+
if (this._colsDirty) {
|
|
1694
|
+
const colsNode = this._buildColsNode();
|
|
1695
|
+
upsertChild('cols', colsNode);
|
|
1696
|
+
}
|
|
1697
|
+
const sheetDataNode = this._buildSheetDataNode();
|
|
1698
|
+
upsertChild('sheetData', sheetDataNode);
|
|
1699
|
+
const mergeCellsNode = this._buildMergeCellsNode();
|
|
1700
|
+
upsertChild('mergeCells', mergeCellsNode);
|
|
1701
|
+
if (this._tablePartsDirty) {
|
|
1702
|
+
const tablePartsNode = this._buildTablePartsNode();
|
|
1703
|
+
upsertChild('tableParts', tablePartsNode);
|
|
1704
|
+
}
|
|
1705
|
+
return worksheet;
|
|
1706
|
+
}
|
|
972
1707
|
/**
|
|
973
1708
|
* Build a cell XML node from a Cell object
|
|
974
1709
|
*/ _buildCellNode(cell) {
|
|
@@ -1013,15 +1748,28 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1013
1748
|
const parsed = parseXml(xml);
|
|
1014
1749
|
const sst = findElement(parsed, 'sst');
|
|
1015
1750
|
if (!sst) return ss;
|
|
1751
|
+
const countAttr = getAttr(sst, 'count');
|
|
1752
|
+
if (countAttr) {
|
|
1753
|
+
const total = parseInt(countAttr, 10);
|
|
1754
|
+
if (Number.isFinite(total) && total >= 0) {
|
|
1755
|
+
ss._totalCount = total;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1016
1758
|
const children = getChildren(sst, 'sst');
|
|
1017
1759
|
for (const child of children){
|
|
1018
1760
|
if ('si' in child) {
|
|
1019
1761
|
const siChildren = getChildren(child, 'si');
|
|
1020
1762
|
const text = ss.extractText(siChildren);
|
|
1021
|
-
ss.
|
|
1022
|
-
|
|
1763
|
+
ss.entries.push({
|
|
1764
|
+
text,
|
|
1765
|
+
node: child
|
|
1766
|
+
});
|
|
1767
|
+
ss.stringToIndex.set(text, ss.entries.length - 1);
|
|
1023
1768
|
}
|
|
1024
1769
|
}
|
|
1770
|
+
if (ss._totalCount === 0 && ss.entries.length > 0) {
|
|
1771
|
+
ss._totalCount = ss.entries.length;
|
|
1772
|
+
}
|
|
1025
1773
|
return ss;
|
|
1026
1774
|
}
|
|
1027
1775
|
/**
|
|
@@ -1058,7 +1806,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1058
1806
|
/**
|
|
1059
1807
|
* Get a string by index
|
|
1060
1808
|
*/ getString(index) {
|
|
1061
|
-
return this.
|
|
1809
|
+
return this.entries[index]?.text;
|
|
1062
1810
|
}
|
|
1063
1811
|
/**
|
|
1064
1812
|
* Add a string and return its index
|
|
@@ -1066,11 +1814,25 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1066
1814
|
*/ addString(str) {
|
|
1067
1815
|
const existing = this.stringToIndex.get(str);
|
|
1068
1816
|
if (existing !== undefined) {
|
|
1817
|
+
this._totalCount++;
|
|
1818
|
+
this._dirty = true;
|
|
1069
1819
|
return existing;
|
|
1070
1820
|
}
|
|
1071
|
-
const index = this.
|
|
1072
|
-
|
|
1821
|
+
const index = this.entries.length;
|
|
1822
|
+
const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? {
|
|
1823
|
+
'xml:space': 'preserve'
|
|
1824
|
+
} : {}, [
|
|
1825
|
+
createText(str)
|
|
1826
|
+
]);
|
|
1827
|
+
const siElement = createElement('si', {}, [
|
|
1828
|
+
tElement
|
|
1829
|
+
]);
|
|
1830
|
+
this.entries.push({
|
|
1831
|
+
text: str,
|
|
1832
|
+
node: siElement
|
|
1833
|
+
});
|
|
1073
1834
|
this.stringToIndex.set(str, index);
|
|
1835
|
+
this._totalCount++;
|
|
1074
1836
|
this._dirty = true;
|
|
1075
1837
|
return index;
|
|
1076
1838
|
}
|
|
@@ -1082,36 +1844,48 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1082
1844
|
/**
|
|
1083
1845
|
* Get the count of strings
|
|
1084
1846
|
*/ get count() {
|
|
1085
|
-
return this.
|
|
1847
|
+
return this.entries.length;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Get total usage count of shared strings
|
|
1851
|
+
*/ get totalCount() {
|
|
1852
|
+
return Math.max(this._totalCount, this.entries.length);
|
|
1086
1853
|
}
|
|
1087
1854
|
/**
|
|
1088
1855
|
* Generate XML for the shared strings table
|
|
1089
1856
|
*/ toXml() {
|
|
1090
1857
|
const siElements = [];
|
|
1091
|
-
for (const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1858
|
+
for (const entry of this.entries){
|
|
1859
|
+
if (entry.node) {
|
|
1860
|
+
siElements.push(entry.node);
|
|
1861
|
+
} else {
|
|
1862
|
+
const str = entry.text;
|
|
1863
|
+
const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? {
|
|
1864
|
+
'xml:space': 'preserve'
|
|
1865
|
+
} : {}, [
|
|
1866
|
+
createText(str)
|
|
1867
|
+
]);
|
|
1868
|
+
const siElement = createElement('si', {}, [
|
|
1869
|
+
tElement
|
|
1870
|
+
]);
|
|
1871
|
+
siElements.push(siElement);
|
|
1872
|
+
}
|
|
1101
1873
|
}
|
|
1874
|
+
const totalCount = Math.max(this._totalCount, this.entries.length);
|
|
1102
1875
|
const sst = createElement('sst', {
|
|
1103
1876
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
1104
|
-
count: String(
|
|
1105
|
-
uniqueCount: String(this.
|
|
1877
|
+
count: String(totalCount),
|
|
1878
|
+
uniqueCount: String(this.entries.length)
|
|
1106
1879
|
}, siElements);
|
|
1107
1880
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
1108
1881
|
sst
|
|
1109
1882
|
])}`;
|
|
1110
1883
|
}
|
|
1111
1884
|
constructor(){
|
|
1112
|
-
this.
|
|
1885
|
+
this.entries = [];
|
|
1113
1886
|
this.stringToIndex = new Map();
|
|
1114
1887
|
this._dirty = false;
|
|
1888
|
+
this._totalCount = 0;
|
|
1115
1889
|
}
|
|
1116
1890
|
}
|
|
1117
1891
|
|
|
@@ -1257,6 +2031,50 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1257
2031
|
* Manages the styles (xl/styles.xml)
|
|
1258
2032
|
*/ class Styles {
|
|
1259
2033
|
/**
|
|
2034
|
+
* Generate a deterministic cache key for a style object.
|
|
2035
|
+
* More efficient than JSON.stringify as it avoids the overhead of
|
|
2036
|
+
* full JSON serialization and produces a consistent key regardless
|
|
2037
|
+
* of property order.
|
|
2038
|
+
*/ _getStyleKey(style) {
|
|
2039
|
+
// Use a delimiter that won't appear in values
|
|
2040
|
+
const SEP = '\x00';
|
|
2041
|
+
// Build key from all style properties in a fixed order
|
|
2042
|
+
const parts = [
|
|
2043
|
+
style.bold ? '1' : '0',
|
|
2044
|
+
style.italic ? '1' : '0',
|
|
2045
|
+
style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',
|
|
2046
|
+
style.strike ? '1' : '0',
|
|
2047
|
+
style.fontSize?.toString() ?? '',
|
|
2048
|
+
style.fontName ?? '',
|
|
2049
|
+
style.fontColor ?? '',
|
|
2050
|
+
style.fontColorTheme?.toString() ?? '',
|
|
2051
|
+
style.fontColorTint?.toString() ?? '',
|
|
2052
|
+
style.fontColorIndexed?.toString() ?? '',
|
|
2053
|
+
style.fill ?? '',
|
|
2054
|
+
style.fillTheme?.toString() ?? '',
|
|
2055
|
+
style.fillTint?.toString() ?? '',
|
|
2056
|
+
style.fillIndexed?.toString() ?? '',
|
|
2057
|
+
style.fillBgColor ?? '',
|
|
2058
|
+
style.fillBgTheme?.toString() ?? '',
|
|
2059
|
+
style.fillBgTint?.toString() ?? '',
|
|
2060
|
+
style.fillBgIndexed?.toString() ?? '',
|
|
2061
|
+
style.numberFormat ?? ''
|
|
2062
|
+
];
|
|
2063
|
+
// Border properties
|
|
2064
|
+
if (style.border) {
|
|
2065
|
+
parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');
|
|
2066
|
+
} else {
|
|
2067
|
+
parts.push('', '', '', '');
|
|
2068
|
+
}
|
|
2069
|
+
// Alignment properties
|
|
2070
|
+
if (style.alignment) {
|
|
2071
|
+
parts.push(style.alignment.horizontal ?? '', style.alignment.vertical ?? '', style.alignment.wrapText ? '1' : '0', style.alignment.textRotation?.toString() ?? '');
|
|
2072
|
+
} else {
|
|
2073
|
+
parts.push('', '', '0', '');
|
|
2074
|
+
}
|
|
2075
|
+
return parts.join(SEP);
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
1260
2078
|
* Parse styles from XML content
|
|
1261
2079
|
*/ static parse(xml) {
|
|
1262
2080
|
const styles = new Styles();
|
|
@@ -1361,7 +2179,18 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1361
2179
|
if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');
|
|
1362
2180
|
if ('name' in child) font.name = getAttr(child, 'val');
|
|
1363
2181
|
if ('color' in child) {
|
|
1364
|
-
|
|
2182
|
+
const color = {};
|
|
2183
|
+
const rgb = getAttr(child, 'rgb');
|
|
2184
|
+
const theme = getAttr(child, 'theme');
|
|
2185
|
+
const tint = getAttr(child, 'tint');
|
|
2186
|
+
const indexed = getAttr(child, 'indexed');
|
|
2187
|
+
if (rgb) color.rgb = rgb;
|
|
2188
|
+
if (theme) color.theme = theme;
|
|
2189
|
+
if (tint) color.tint = tint;
|
|
2190
|
+
if (indexed) color.indexed = indexed;
|
|
2191
|
+
if (color.rgb || color.theme || color.tint || color.indexed) {
|
|
2192
|
+
font.color = color;
|
|
2193
|
+
}
|
|
1365
2194
|
}
|
|
1366
2195
|
}
|
|
1367
2196
|
return font;
|
|
@@ -1378,10 +2207,32 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1378
2207
|
const pfChildren = getChildren(child, 'patternFill');
|
|
1379
2208
|
for (const pfChild of pfChildren){
|
|
1380
2209
|
if ('fgColor' in pfChild) {
|
|
1381
|
-
|
|
2210
|
+
const color = {};
|
|
2211
|
+
const rgb = getAttr(pfChild, 'rgb');
|
|
2212
|
+
const theme = getAttr(pfChild, 'theme');
|
|
2213
|
+
const tint = getAttr(pfChild, 'tint');
|
|
2214
|
+
const indexed = getAttr(pfChild, 'indexed');
|
|
2215
|
+
if (rgb) color.rgb = rgb;
|
|
2216
|
+
if (theme) color.theme = theme;
|
|
2217
|
+
if (tint) color.tint = tint;
|
|
2218
|
+
if (indexed) color.indexed = indexed;
|
|
2219
|
+
if (color.rgb || color.theme || color.tint || color.indexed) {
|
|
2220
|
+
fill.fgColor = color;
|
|
2221
|
+
}
|
|
1382
2222
|
}
|
|
1383
2223
|
if ('bgColor' in pfChild) {
|
|
1384
|
-
|
|
2224
|
+
const color = {};
|
|
2225
|
+
const rgb = getAttr(pfChild, 'rgb');
|
|
2226
|
+
const theme = getAttr(pfChild, 'theme');
|
|
2227
|
+
const tint = getAttr(pfChild, 'tint');
|
|
2228
|
+
const indexed = getAttr(pfChild, 'indexed');
|
|
2229
|
+
if (rgb) color.rgb = rgb;
|
|
2230
|
+
if (theme) color.theme = theme;
|
|
2231
|
+
if (tint) color.tint = tint;
|
|
2232
|
+
if (indexed) color.indexed = indexed;
|
|
2233
|
+
if (color.rgb || color.theme || color.tint || color.indexed) {
|
|
2234
|
+
fill.bgColor = color;
|
|
2235
|
+
}
|
|
1385
2236
|
}
|
|
1386
2237
|
}
|
|
1387
2238
|
}
|
|
@@ -1423,6 +2274,10 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1423
2274
|
/**
|
|
1424
2275
|
* Get a style by index
|
|
1425
2276
|
*/ getStyle(index) {
|
|
2277
|
+
const cached = this._styleObjectCache.get(index);
|
|
2278
|
+
if (cached) return {
|
|
2279
|
+
...cached
|
|
2280
|
+
};
|
|
1426
2281
|
const xf = this._cellXfs[index];
|
|
1427
2282
|
if (!xf) return {};
|
|
1428
2283
|
const font = this._fonts[xf.fontId];
|
|
@@ -1438,10 +2293,22 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1438
2293
|
if (font.strike) style.strike = true;
|
|
1439
2294
|
if (font.size) style.fontSize = font.size;
|
|
1440
2295
|
if (font.name) style.fontName = font.name;
|
|
1441
|
-
if (font.color) style.fontColor = font.color;
|
|
2296
|
+
if (font.color?.rgb) style.fontColor = font.color.rgb;
|
|
2297
|
+
if (font.color?.theme) style.fontColorTheme = Number(font.color.theme);
|
|
2298
|
+
if (font.color?.tint) style.fontColorTint = Number(font.color.tint);
|
|
2299
|
+
if (font.color?.indexed) style.fontColorIndexed = Number(font.color.indexed);
|
|
1442
2300
|
}
|
|
1443
2301
|
if (fill && fill.fgColor) {
|
|
1444
|
-
style.fill = fill.fgColor;
|
|
2302
|
+
if (fill.fgColor.rgb) style.fill = fill.fgColor.rgb;
|
|
2303
|
+
if (fill.fgColor.theme) style.fillTheme = Number(fill.fgColor.theme);
|
|
2304
|
+
if (fill.fgColor.tint) style.fillTint = Number(fill.fgColor.tint);
|
|
2305
|
+
if (fill.fgColor.indexed) style.fillIndexed = Number(fill.fgColor.indexed);
|
|
2306
|
+
}
|
|
2307
|
+
if (fill && fill.bgColor) {
|
|
2308
|
+
if (fill.bgColor.rgb) style.fillBgColor = fill.bgColor.rgb;
|
|
2309
|
+
if (fill.bgColor.theme) style.fillBgTheme = Number(fill.bgColor.theme);
|
|
2310
|
+
if (fill.bgColor.tint) style.fillBgTint = Number(fill.bgColor.tint);
|
|
2311
|
+
if (fill.bgColor.indexed) style.fillBgIndexed = Number(fill.bgColor.indexed);
|
|
1445
2312
|
}
|
|
1446
2313
|
if (border) {
|
|
1447
2314
|
if (border.top || border.bottom || border.left || border.right) {
|
|
@@ -1464,13 +2331,16 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1464
2331
|
textRotation: xf.alignment.textRotation
|
|
1465
2332
|
};
|
|
1466
2333
|
}
|
|
2334
|
+
this._styleObjectCache.set(index, {
|
|
2335
|
+
...style
|
|
2336
|
+
});
|
|
1467
2337
|
return style;
|
|
1468
2338
|
}
|
|
1469
2339
|
/**
|
|
1470
2340
|
* Create a style and return its index
|
|
1471
2341
|
* Uses caching to deduplicate identical styles
|
|
1472
2342
|
*/ createStyle(style) {
|
|
1473
|
-
const key =
|
|
2343
|
+
const key = this._getStyleKey(style);
|
|
1474
2344
|
const cached = this._styleCache.get(key);
|
|
1475
2345
|
if (cached !== undefined) {
|
|
1476
2346
|
return cached;
|
|
@@ -1502,9 +2372,22 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1502
2372
|
const index = this._cellXfs.length;
|
|
1503
2373
|
this._cellXfs.push(xf);
|
|
1504
2374
|
this._styleCache.set(key, index);
|
|
2375
|
+
this._styleObjectCache.set(index, {
|
|
2376
|
+
...style
|
|
2377
|
+
});
|
|
1505
2378
|
return index;
|
|
1506
2379
|
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Clone an existing style by index, optionally overriding fields.
|
|
2382
|
+
*/ cloneStyle(index, overrides = {}) {
|
|
2383
|
+
const baseStyle = this.getStyle(index);
|
|
2384
|
+
return this.createStyle({
|
|
2385
|
+
...baseStyle,
|
|
2386
|
+
...overrides
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
1507
2389
|
_findOrCreateFont(style) {
|
|
2390
|
+
const color = this._toStyleColor(style.fontColor, style.fontColorTheme, style.fontColorTint, style.fontColorIndexed);
|
|
1508
2391
|
const font = {
|
|
1509
2392
|
bold: style.bold || false,
|
|
1510
2393
|
italic: style.italic || false,
|
|
@@ -1512,12 +2395,12 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1512
2395
|
strike: style.strike || false,
|
|
1513
2396
|
size: style.fontSize,
|
|
1514
2397
|
name: style.fontName,
|
|
1515
|
-
color
|
|
2398
|
+
color
|
|
1516
2399
|
};
|
|
1517
2400
|
// Try to find existing font
|
|
1518
2401
|
for(let i = 0; i < this._fonts.length; i++){
|
|
1519
2402
|
const f = this._fonts[i];
|
|
1520
|
-
if (f.bold === font.bold && f.italic === font.italic && f.underline === font.underline && f.strike === font.strike && f.size === font.size && f.name === font.name && f.color
|
|
2403
|
+
if (f.bold === font.bold && f.italic === font.italic && f.underline === font.underline && f.strike === font.strike && f.size === font.size && f.name === font.name && this._colorsEqual(f.color, font.color)) {
|
|
1521
2404
|
return i;
|
|
1522
2405
|
}
|
|
1523
2406
|
}
|
|
@@ -1526,18 +2409,21 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1526
2409
|
return this._fonts.length - 1;
|
|
1527
2410
|
}
|
|
1528
2411
|
_findOrCreateFill(style) {
|
|
1529
|
-
|
|
2412
|
+
const fgColor = this._toStyleColor(style.fill, style.fillTheme, style.fillTint, style.fillIndexed);
|
|
2413
|
+
const bgColor = this._toStyleColor(style.fillBgColor, style.fillBgTheme, style.fillBgTint, style.fillBgIndexed);
|
|
2414
|
+
if (!fgColor && !bgColor) return 0;
|
|
1530
2415
|
// Try to find existing fill
|
|
1531
2416
|
for(let i = 0; i < this._fills.length; i++){
|
|
1532
2417
|
const f = this._fills[i];
|
|
1533
|
-
if (f.fgColor
|
|
2418
|
+
if (this._colorsEqual(f.fgColor, fgColor) && this._colorsEqual(f.bgColor, bgColor)) {
|
|
1534
2419
|
return i;
|
|
1535
2420
|
}
|
|
1536
2421
|
}
|
|
1537
2422
|
// Create new fill
|
|
1538
2423
|
this._fills.push({
|
|
1539
2424
|
type: 'solid',
|
|
1540
|
-
fgColor:
|
|
2425
|
+
fgColor: fgColor || undefined,
|
|
2426
|
+
bgColor: bgColor || undefined
|
|
1541
2427
|
});
|
|
1542
2428
|
return this._fills.length - 1;
|
|
1543
2429
|
}
|
|
@@ -1663,9 +2549,16 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1663
2549
|
if (font.size) children.push(createElement('sz', {
|
|
1664
2550
|
val: String(font.size)
|
|
1665
2551
|
}, []));
|
|
1666
|
-
if (font.color)
|
|
1667
|
-
|
|
1668
|
-
|
|
2552
|
+
if (font.color) {
|
|
2553
|
+
const attrs = {};
|
|
2554
|
+
if (font.color.rgb) attrs.rgb = normalizeColor(font.color.rgb);
|
|
2555
|
+
if (font.color.theme) attrs.theme = font.color.theme;
|
|
2556
|
+
if (font.color.tint) attrs.tint = font.color.tint;
|
|
2557
|
+
if (font.color.indexed) attrs.indexed = font.color.indexed;
|
|
2558
|
+
if (Object.keys(attrs).length > 0) {
|
|
2559
|
+
children.push(createElement('color', attrs, []));
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
1669
2562
|
if (font.name) children.push(createElement('name', {
|
|
1670
2563
|
val: font.name
|
|
1671
2564
|
}, []));
|
|
@@ -1674,22 +2567,30 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1674
2567
|
_buildFillNode(fill) {
|
|
1675
2568
|
const patternChildren = [];
|
|
1676
2569
|
if (fill.fgColor) {
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2570
|
+
const attrs = {};
|
|
2571
|
+
if (fill.fgColor.rgb) attrs.rgb = normalizeColor(fill.fgColor.rgb);
|
|
2572
|
+
if (fill.fgColor.theme) attrs.theme = fill.fgColor.theme;
|
|
2573
|
+
if (fill.fgColor.tint) attrs.tint = fill.fgColor.tint;
|
|
2574
|
+
if (fill.fgColor.indexed) attrs.indexed = fill.fgColor.indexed;
|
|
2575
|
+
if (Object.keys(attrs).length > 0) {
|
|
2576
|
+
patternChildren.push(createElement('fgColor', attrs, []));
|
|
2577
|
+
}
|
|
1681
2578
|
// For solid fills, bgColor is required (indexed 64 = system background)
|
|
1682
|
-
if (fill.type === 'solid') {
|
|
2579
|
+
if (fill.type === 'solid' && !fill.bgColor) {
|
|
1683
2580
|
patternChildren.push(createElement('bgColor', {
|
|
1684
2581
|
indexed: '64'
|
|
1685
2582
|
}, []));
|
|
1686
2583
|
}
|
|
1687
2584
|
}
|
|
1688
|
-
if (fill.bgColor
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2585
|
+
if (fill.bgColor) {
|
|
2586
|
+
const attrs = {};
|
|
2587
|
+
if (fill.bgColor.rgb) attrs.rgb = normalizeColor(fill.bgColor.rgb);
|
|
2588
|
+
if (fill.bgColor.theme) attrs.theme = fill.bgColor.theme;
|
|
2589
|
+
if (fill.bgColor.tint) attrs.tint = fill.bgColor.tint;
|
|
2590
|
+
if (fill.bgColor.indexed) attrs.indexed = fill.bgColor.indexed;
|
|
2591
|
+
if (Object.keys(attrs).length > 0) {
|
|
2592
|
+
patternChildren.push(createElement('bgColor', attrs, []));
|
|
2593
|
+
}
|
|
1693
2594
|
}
|
|
1694
2595
|
const patternFill = createElement('patternFill', {
|
|
1695
2596
|
patternType: fill.type || 'none'
|
|
@@ -1698,6 +2599,24 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1698
2599
|
patternFill
|
|
1699
2600
|
]);
|
|
1700
2601
|
}
|
|
2602
|
+
_toStyleColor(rgb, theme, tint, indexed) {
|
|
2603
|
+
if (rgb) {
|
|
2604
|
+
return {
|
|
2605
|
+
rgb
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
const color = {};
|
|
2609
|
+
if (theme !== undefined) color.theme = String(theme);
|
|
2610
|
+
if (tint !== undefined) color.tint = String(tint);
|
|
2611
|
+
if (indexed !== undefined) color.indexed = String(indexed);
|
|
2612
|
+
if (color.theme || color.tint || color.indexed) return color;
|
|
2613
|
+
return undefined;
|
|
2614
|
+
}
|
|
2615
|
+
_colorsEqual(a, b) {
|
|
2616
|
+
if (!a && !b) return true;
|
|
2617
|
+
if (!a || !b) return false;
|
|
2618
|
+
return a.rgb === b.rgb && a.theme === b.theme && a.tint === b.tint && a.indexed === b.indexed;
|
|
2619
|
+
}
|
|
1701
2620
|
_buildBorderNode(border) {
|
|
1702
2621
|
const children = [];
|
|
1703
2622
|
if (border.left) children.push(createElement('left', {
|
|
@@ -1753,17 +2672,19 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1753
2672
|
this._dirty = false;
|
|
1754
2673
|
// Cache for style deduplication
|
|
1755
2674
|
this._styleCache = new Map();
|
|
2675
|
+
this._styleObjectCache = new Map();
|
|
1756
2676
|
}
|
|
1757
2677
|
}
|
|
1758
2678
|
|
|
1759
2679
|
/**
|
|
1760
2680
|
* Represents an Excel pivot table with a fluent API for configuration.
|
|
1761
2681
|
*/ class PivotTable {
|
|
1762
|
-
constructor(name, cache, targetSheet, targetCell, targetRow, targetCol, pivotTableIndex){
|
|
2682
|
+
constructor(name, cache, targetSheet, targetCell, targetRow, targetCol, pivotTableIndex, cacheFileIndex){
|
|
1763
2683
|
this._rowFields = [];
|
|
1764
2684
|
this._columnFields = [];
|
|
1765
2685
|
this._valueFields = [];
|
|
1766
2686
|
this._filterFields = [];
|
|
2687
|
+
this._fieldAssignments = new Map();
|
|
1767
2688
|
this._styles = null;
|
|
1768
2689
|
this._name = name;
|
|
1769
2690
|
this._cache = cache;
|
|
@@ -1772,6 +2693,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1772
2693
|
this._targetRow = targetRow;
|
|
1773
2694
|
this._targetCol = targetCol;
|
|
1774
2695
|
this._pivotTableIndex = pivotTableIndex;
|
|
2696
|
+
this._cacheFileIndex = cacheFileIndex;
|
|
1775
2697
|
}
|
|
1776
2698
|
/**
|
|
1777
2699
|
* Get the pivot table name
|
|
@@ -1799,6 +2721,12 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1799
2721
|
return this._pivotTableIndex;
|
|
1800
2722
|
}
|
|
1801
2723
|
/**
|
|
2724
|
+
* Get the pivot cache file index used for rels.
|
|
2725
|
+
* @internal
|
|
2726
|
+
*/ get cacheFileIndex() {
|
|
2727
|
+
return this._cacheFileIndex;
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
1802
2730
|
* Set the styles reference for number format resolution
|
|
1803
2731
|
* @internal
|
|
1804
2732
|
*/ setStyles(styles) {
|
|
@@ -1813,11 +2741,13 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1813
2741
|
if (fieldIndex < 0) {
|
|
1814
2742
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1815
2743
|
}
|
|
1816
|
-
|
|
2744
|
+
const assignment = {
|
|
1817
2745
|
fieldName,
|
|
1818
2746
|
fieldIndex,
|
|
1819
2747
|
axis: 'row'
|
|
1820
|
-
}
|
|
2748
|
+
};
|
|
2749
|
+
this._rowFields.push(assignment);
|
|
2750
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1821
2751
|
return this;
|
|
1822
2752
|
}
|
|
1823
2753
|
/**
|
|
@@ -1828,33 +2758,52 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1828
2758
|
if (fieldIndex < 0) {
|
|
1829
2759
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1830
2760
|
}
|
|
1831
|
-
|
|
2761
|
+
const assignment = {
|
|
1832
2762
|
fieldName,
|
|
1833
2763
|
fieldIndex,
|
|
1834
2764
|
axis: 'column'
|
|
1835
|
-
}
|
|
2765
|
+
};
|
|
2766
|
+
this._columnFields.push(assignment);
|
|
2767
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1836
2768
|
return this;
|
|
1837
2769
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2770
|
+
addValueField(fieldNameOrConfig, aggregation = 'sum', displayName, numberFormat) {
|
|
2771
|
+
// Normalize arguments to a common form
|
|
2772
|
+
let fieldName;
|
|
2773
|
+
let agg;
|
|
2774
|
+
let name;
|
|
2775
|
+
let format;
|
|
2776
|
+
if (typeof fieldNameOrConfig === 'object') {
|
|
2777
|
+
fieldName = fieldNameOrConfig.field;
|
|
2778
|
+
agg = fieldNameOrConfig.aggregation ?? 'sum';
|
|
2779
|
+
name = fieldNameOrConfig.name;
|
|
2780
|
+
format = fieldNameOrConfig.numberFormat;
|
|
2781
|
+
} else {
|
|
2782
|
+
fieldName = fieldNameOrConfig;
|
|
2783
|
+
agg = aggregation;
|
|
2784
|
+
name = displayName;
|
|
2785
|
+
format = numberFormat;
|
|
2786
|
+
}
|
|
1845
2787
|
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
1846
2788
|
if (fieldIndex < 0) {
|
|
1847
2789
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1848
2790
|
}
|
|
1849
|
-
const defaultName = `${
|
|
1850
|
-
|
|
2791
|
+
const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
|
|
2792
|
+
// Resolve numFmtId immediately if format is provided and styles are available
|
|
2793
|
+
let numFmtId;
|
|
2794
|
+
if (format && this._styles) {
|
|
2795
|
+
numFmtId = this._styles.getOrCreateNumFmtId(format);
|
|
2796
|
+
}
|
|
2797
|
+
const assignment = {
|
|
1851
2798
|
fieldName,
|
|
1852
2799
|
fieldIndex,
|
|
1853
2800
|
axis: 'value',
|
|
1854
|
-
aggregation,
|
|
1855
|
-
displayName:
|
|
1856
|
-
|
|
1857
|
-
}
|
|
2801
|
+
aggregation: agg,
|
|
2802
|
+
displayName: name || defaultName,
|
|
2803
|
+
numFmtId
|
|
2804
|
+
};
|
|
2805
|
+
this._valueFields.push(assignment);
|
|
2806
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
1858
2807
|
return this;
|
|
1859
2808
|
}
|
|
1860
2809
|
/**
|
|
@@ -1865,11 +2814,51 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1865
2814
|
if (fieldIndex < 0) {
|
|
1866
2815
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
1867
2816
|
}
|
|
1868
|
-
|
|
2817
|
+
const assignment = {
|
|
1869
2818
|
fieldName,
|
|
1870
2819
|
fieldIndex,
|
|
1871
2820
|
axis: 'filter'
|
|
1872
|
-
}
|
|
2821
|
+
};
|
|
2822
|
+
this._filterFields.push(assignment);
|
|
2823
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
2824
|
+
return this;
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Set a sort order for a row or column field
|
|
2828
|
+
* @param fieldName - Name of the field to sort
|
|
2829
|
+
* @param order - Sort order ('asc' or 'desc')
|
|
2830
|
+
*/ sortField(fieldName, order) {
|
|
2831
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
2832
|
+
if (fieldIndex < 0) {
|
|
2833
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
2834
|
+
}
|
|
2835
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
2836
|
+
if (!assignment) {
|
|
2837
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
2838
|
+
}
|
|
2839
|
+
if (assignment.axis !== 'row' && assignment.axis !== 'column') {
|
|
2840
|
+
throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
|
|
2841
|
+
}
|
|
2842
|
+
assignment.sortOrder = order;
|
|
2843
|
+
return this;
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Filter items for a row, column, or filter field
|
|
2847
|
+
* @param fieldName - Name of the field to filter
|
|
2848
|
+
* @param filter - Filter configuration with include or exclude list
|
|
2849
|
+
*/ filterField(fieldName, filter) {
|
|
2850
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
2851
|
+
if (fieldIndex < 0) {
|
|
2852
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
2853
|
+
}
|
|
2854
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
2855
|
+
if (!assignment) {
|
|
2856
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
2857
|
+
}
|
|
2858
|
+
if (filter.include && filter.exclude) {
|
|
2859
|
+
throw new Error('Cannot use both include and exclude in the same filter');
|
|
2860
|
+
}
|
|
2861
|
+
assignment.filter = filter;
|
|
1873
2862
|
return this;
|
|
1874
2863
|
}
|
|
1875
2864
|
/**
|
|
@@ -1984,9 +2973,9 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1984
2973
|
baseItem: '0',
|
|
1985
2974
|
subtotal: f.aggregation || 'sum'
|
|
1986
2975
|
};
|
|
1987
|
-
// Add numFmtId if
|
|
1988
|
-
if (f.
|
|
1989
|
-
attrs.numFmtId = String(
|
|
2976
|
+
// Add numFmtId if it was resolved during addValueField
|
|
2977
|
+
if (f.numFmtId !== undefined) {
|
|
2978
|
+
attrs.numFmtId = String(f.numFmtId);
|
|
1990
2979
|
}
|
|
1991
2980
|
return createElement('dataField', attrs, []);
|
|
1992
2981
|
});
|
|
@@ -1995,7 +2984,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
1995
2984
|
}, dataFieldNodes));
|
|
1996
2985
|
}
|
|
1997
2986
|
// Check if any value field has a number format
|
|
1998
|
-
const hasNumberFormats = this._valueFields.some((f)=>f.
|
|
2987
|
+
const hasNumberFormats = this._valueFields.some((f)=>f.numFmtId !== undefined);
|
|
1999
2988
|
// Pivot table style
|
|
2000
2989
|
children.push(createElement('pivotTableStyleInfo', {
|
|
2001
2990
|
name: 'PivotStyleMedium9',
|
|
@@ -2043,22 +3032,19 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2043
3032
|
const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
2044
3033
|
const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
2045
3034
|
const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
|
|
3035
|
+
// Get the assignment to check for sort/filter options
|
|
3036
|
+
const assignment = rowField || colField || filterField;
|
|
2046
3037
|
if (rowField) {
|
|
2047
3038
|
attrs.axis = 'axisRow';
|
|
2048
3039
|
attrs.showAll = '0';
|
|
3040
|
+
// Add sort order if specified
|
|
3041
|
+
if (rowField.sortOrder) {
|
|
3042
|
+
attrs.sortType = rowField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
3043
|
+
}
|
|
2049
3044
|
// Add items for shared values
|
|
2050
3045
|
const cacheField = this._cache.fields[fieldIndex];
|
|
2051
3046
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
2052
|
-
const itemNodes =
|
|
2053
|
-
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
2054
|
-
itemNodes.push(createElement('item', {
|
|
2055
|
-
x: String(i)
|
|
2056
|
-
}, []));
|
|
2057
|
-
}
|
|
2058
|
-
// Add default subtotal item
|
|
2059
|
-
itemNodes.push(createElement('item', {
|
|
2060
|
-
t: 'default'
|
|
2061
|
-
}, []));
|
|
3047
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
2062
3048
|
children.push(createElement('items', {
|
|
2063
3049
|
count: String(itemNodes.length)
|
|
2064
3050
|
}, itemNodes));
|
|
@@ -2066,17 +3052,13 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2066
3052
|
} else if (colField) {
|
|
2067
3053
|
attrs.axis = 'axisCol';
|
|
2068
3054
|
attrs.showAll = '0';
|
|
3055
|
+
// Add sort order if specified
|
|
3056
|
+
if (colField.sortOrder) {
|
|
3057
|
+
attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
3058
|
+
}
|
|
2069
3059
|
const cacheField = this._cache.fields[fieldIndex];
|
|
2070
3060
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
2071
|
-
const itemNodes =
|
|
2072
|
-
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
2073
|
-
itemNodes.push(createElement('item', {
|
|
2074
|
-
x: String(i)
|
|
2075
|
-
}, []));
|
|
2076
|
-
}
|
|
2077
|
-
itemNodes.push(createElement('item', {
|
|
2078
|
-
t: 'default'
|
|
2079
|
-
}, []));
|
|
3061
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
2080
3062
|
children.push(createElement('items', {
|
|
2081
3063
|
count: String(itemNodes.length)
|
|
2082
3064
|
}, itemNodes));
|
|
@@ -2086,15 +3068,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2086
3068
|
attrs.showAll = '0';
|
|
2087
3069
|
const cacheField = this._cache.fields[fieldIndex];
|
|
2088
3070
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
2089
|
-
const itemNodes =
|
|
2090
|
-
for(let i = 0; i < cacheField.sharedItems.length; i++){
|
|
2091
|
-
itemNodes.push(createElement('item', {
|
|
2092
|
-
x: String(i)
|
|
2093
|
-
}, []));
|
|
2094
|
-
}
|
|
2095
|
-
itemNodes.push(createElement('item', {
|
|
2096
|
-
t: 'default'
|
|
2097
|
-
}, []));
|
|
3071
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
2098
3072
|
children.push(createElement('items', {
|
|
2099
3073
|
count: String(itemNodes.length)
|
|
2100
3074
|
}, itemNodes));
|
|
@@ -2108,6 +3082,35 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2108
3082
|
return createElement('pivotField', attrs, children);
|
|
2109
3083
|
}
|
|
2110
3084
|
/**
|
|
3085
|
+
* Build item nodes for a pivot field, with optional filtering
|
|
3086
|
+
*/ _buildItemNodes(sharedItems, filter) {
|
|
3087
|
+
const itemNodes = [];
|
|
3088
|
+
for(let i = 0; i < sharedItems.length; i++){
|
|
3089
|
+
const itemValue = sharedItems[i];
|
|
3090
|
+
const itemAttrs = {
|
|
3091
|
+
x: String(i)
|
|
3092
|
+
};
|
|
3093
|
+
// Check if this item should be hidden
|
|
3094
|
+
if (filter) {
|
|
3095
|
+
let hidden = false;
|
|
3096
|
+
if (filter.exclude && filter.exclude.includes(itemValue)) {
|
|
3097
|
+
hidden = true;
|
|
3098
|
+
} else if (filter.include && !filter.include.includes(itemValue)) {
|
|
3099
|
+
hidden = true;
|
|
3100
|
+
}
|
|
3101
|
+
if (hidden) {
|
|
3102
|
+
itemAttrs.h = '1';
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
3106
|
+
}
|
|
3107
|
+
// Add default subtotal item
|
|
3108
|
+
itemNodes.push(createElement('item', {
|
|
3109
|
+
t: 'default'
|
|
3110
|
+
}, []));
|
|
3111
|
+
return itemNodes;
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
2111
3114
|
* Build row items based on unique values in row fields
|
|
2112
3115
|
*/ _buildRowItems() {
|
|
2113
3116
|
const items = [];
|
|
@@ -2252,7 +3255,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2252
3255
|
* Manages the pivot cache (definition and records) for a pivot table.
|
|
2253
3256
|
* The cache stores source data metadata and cached values.
|
|
2254
3257
|
*/ class PivotCache {
|
|
2255
|
-
constructor(cacheId, sourceSheet, sourceRange){
|
|
3258
|
+
constructor(cacheId, sourceSheet, sourceRange, fileIndex){
|
|
2256
3259
|
this._fields = [];
|
|
2257
3260
|
this._records = [];
|
|
2258
3261
|
this._recordCount = 0;
|
|
@@ -2260,6 +3263,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2260
3263
|
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
2261
3264
|
this._sharedItemsIndexMap = new Map();
|
|
2262
3265
|
this._cacheId = cacheId;
|
|
3266
|
+
this._fileIndex = fileIndex;
|
|
2263
3267
|
this._sourceSheet = sourceSheet;
|
|
2264
3268
|
this._sourceRange = sourceRange;
|
|
2265
3269
|
}
|
|
@@ -2269,6 +3273,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2269
3273
|
return this._cacheId;
|
|
2270
3274
|
}
|
|
2271
3275
|
/**
|
|
3276
|
+
* Get the file index for this cache (used for file naming).
|
|
3277
|
+
*/ get fileIndex() {
|
|
3278
|
+
return this._fileIndex;
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
2272
3281
|
* Set refreshOnLoad option
|
|
2273
3282
|
*/ set refreshOnLoad(value) {
|
|
2274
3283
|
this._refreshOnLoad = value;
|
|
@@ -2570,6 +3579,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2570
3579
|
this._pivotTables = [];
|
|
2571
3580
|
this._pivotCaches = [];
|
|
2572
3581
|
this._nextCacheId = 0;
|
|
3582
|
+
this._nextCacheFileIndex = 1;
|
|
3583
|
+
// Table support
|
|
3584
|
+
this._nextTableId = 1;
|
|
3585
|
+
// Date serialization handling
|
|
3586
|
+
this._dateHandling = 'jsDate';
|
|
2573
3587
|
this._sharedStrings = new SharedStrings();
|
|
2574
3588
|
this._styles = Styles.createDefault();
|
|
2575
3589
|
}
|
|
@@ -2634,6 +3648,23 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2634
3648
|
return this._styles;
|
|
2635
3649
|
}
|
|
2636
3650
|
/**
|
|
3651
|
+
* Get the workbook date handling strategy.
|
|
3652
|
+
*/ get dateHandling() {
|
|
3653
|
+
return this._dateHandling;
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Set the workbook date handling strategy.
|
|
3657
|
+
*/ set dateHandling(value) {
|
|
3658
|
+
this._dateHandling = value;
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Get the next unique table ID for this workbook.
|
|
3662
|
+
* Table IDs must be unique across all worksheets.
|
|
3663
|
+
* @internal
|
|
3664
|
+
*/ getNextTableId() {
|
|
3665
|
+
return this._nextTableId++;
|
|
3666
|
+
}
|
|
3667
|
+
/**
|
|
2637
3668
|
* Get a worksheet by name or index
|
|
2638
3669
|
*/ sheet(nameOrIndex) {
|
|
2639
3670
|
let def;
|
|
@@ -2715,6 +3746,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2715
3746
|
const def = this._sheetDefs[index];
|
|
2716
3747
|
this._sheetDefs.splice(index, 1);
|
|
2717
3748
|
this._sheets.delete(def.name);
|
|
3749
|
+
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
3750
|
+
if (rel) {
|
|
3751
|
+
const sheetPath = `xl/${rel.target}`;
|
|
3752
|
+
this._files.delete(sheetPath);
|
|
3753
|
+
}
|
|
2718
3754
|
// Remove relationship
|
|
2719
3755
|
const relIndex = this._relationships.findIndex((r)=>r.id === def.rId);
|
|
2720
3756
|
if (relIndex >= 0) {
|
|
@@ -2757,12 +3793,76 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2757
3793
|
newCell.styleIndex = cell.styleIndex;
|
|
2758
3794
|
}
|
|
2759
3795
|
}
|
|
3796
|
+
// Copy column widths
|
|
3797
|
+
for (const [col, width] of source.getColumnWidths()){
|
|
3798
|
+
copy.setColumnWidth(col, width);
|
|
3799
|
+
}
|
|
3800
|
+
// Copy row heights
|
|
3801
|
+
for (const [row, height] of source.getRowHeights()){
|
|
3802
|
+
copy.setRowHeight(row, height);
|
|
3803
|
+
}
|
|
3804
|
+
// Copy frozen panes
|
|
3805
|
+
const frozen = source.getFrozenPane();
|
|
3806
|
+
if (frozen) {
|
|
3807
|
+
copy.freezePane(frozen.row, frozen.col);
|
|
3808
|
+
}
|
|
2760
3809
|
// Copy merged cells
|
|
2761
3810
|
for (const mergedRange of source.mergedCells){
|
|
2762
3811
|
copy.mergeCells(mergedRange);
|
|
2763
3812
|
}
|
|
3813
|
+
// Copy tables
|
|
3814
|
+
for (const table of source.tables){
|
|
3815
|
+
const tableName = this._createUniqueTableName(table.name, newName);
|
|
3816
|
+
const newTable = copy.createTable({
|
|
3817
|
+
name: tableName,
|
|
3818
|
+
range: table.baseRange,
|
|
3819
|
+
totalRow: table.hasTotalRow,
|
|
3820
|
+
headerRow: table.hasHeaderRow,
|
|
3821
|
+
style: table.style
|
|
3822
|
+
});
|
|
3823
|
+
if (!table.hasAutoFilter) {
|
|
3824
|
+
newTable.setAutoFilter(false);
|
|
3825
|
+
}
|
|
3826
|
+
if (table.hasTotalRow) {
|
|
3827
|
+
for (const columnName of table.columns){
|
|
3828
|
+
const fn = table.getTotalFunction(columnName);
|
|
3829
|
+
if (fn) {
|
|
3830
|
+
newTable.setTotalFunction(columnName, fn);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
2764
3835
|
return copy;
|
|
2765
3836
|
}
|
|
3837
|
+
_createUniqueTableName(base, sheetName) {
|
|
3838
|
+
const normalizedSheet = sheetName.replace(/[^A-Za-z0-9_.]/g, '_');
|
|
3839
|
+
const sanitizedBase = this._sanitizeTableName(`${base}_${normalizedSheet || 'Sheet'}`);
|
|
3840
|
+
let candidate = sanitizedBase;
|
|
3841
|
+
let counter = 1;
|
|
3842
|
+
while(this._hasTableName(candidate)){
|
|
3843
|
+
candidate = `${sanitizedBase}_${counter++}`;
|
|
3844
|
+
}
|
|
3845
|
+
return candidate;
|
|
3846
|
+
}
|
|
3847
|
+
_sanitizeTableName(name) {
|
|
3848
|
+
let result = name.replace(/[^A-Za-z0-9_.]/g, '_');
|
|
3849
|
+
if (!/^[A-Za-z_]/.test(result)) {
|
|
3850
|
+
result = `_${result}`;
|
|
3851
|
+
}
|
|
3852
|
+
if (result.length === 0) {
|
|
3853
|
+
result = 'Table';
|
|
3854
|
+
}
|
|
3855
|
+
return result;
|
|
3856
|
+
}
|
|
3857
|
+
_hasTableName(name) {
|
|
3858
|
+
for (const sheetName of this.sheetNames){
|
|
3859
|
+
const ws = this.sheet(sheetName);
|
|
3860
|
+
for (const table of ws.tables){
|
|
3861
|
+
if (table.name === name) return true;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
return false;
|
|
3865
|
+
}
|
|
2766
3866
|
/**
|
|
2767
3867
|
* Create a new worksheet from an array of objects.
|
|
2768
3868
|
*
|
|
@@ -2934,7 +4034,8 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2934
4034
|
const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
|
|
2935
4035
|
// Create pivot cache
|
|
2936
4036
|
const cacheId = this._nextCacheId++;
|
|
2937
|
-
const
|
|
4037
|
+
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
4038
|
+
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
2938
4039
|
cache.buildFromData(headers, data);
|
|
2939
4040
|
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
2940
4041
|
if (config.refreshOnLoad === false) {
|
|
@@ -2943,7 +4044,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2943
4044
|
this._pivotCaches.push(cache);
|
|
2944
4045
|
// Create pivot table
|
|
2945
4046
|
const pivotTableIndex = this._pivotTables.length + 1;
|
|
2946
|
-
const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex);
|
|
4047
|
+
const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex, cacheFileIndex);
|
|
2947
4048
|
// Set styles reference for number format resolution
|
|
2948
4049
|
pivotTable.setStyles(this._styles);
|
|
2949
4050
|
this._pivotTables.push(pivotTable);
|
|
@@ -3042,10 +4143,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3042
4143
|
}
|
|
3043
4144
|
}
|
|
3044
4145
|
_updateFiles() {
|
|
4146
|
+
const relationshipInfo = this._buildRelationshipInfo();
|
|
3045
4147
|
// Update workbook.xml
|
|
3046
|
-
this._updateWorkbookXml();
|
|
4148
|
+
this._updateWorkbookXml(relationshipInfo.pivotCacheRelIds);
|
|
3047
4149
|
// Update relationships
|
|
3048
|
-
this._updateRelationshipsXml();
|
|
4150
|
+
this._updateRelationshipsXml(relationshipInfo.relNodes);
|
|
3049
4151
|
// Update content types
|
|
3050
4152
|
this._updateContentTypes();
|
|
3051
4153
|
// Update shared strings if modified
|
|
@@ -3056,9 +4158,9 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3056
4158
|
if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
|
|
3057
4159
|
writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
|
|
3058
4160
|
}
|
|
3059
|
-
// Update worksheets
|
|
4161
|
+
// Update worksheets (needed for pivot table targets)
|
|
3060
4162
|
for (const [name, worksheet] of this._sheets){
|
|
3061
|
-
if (worksheet.dirty || this._dirty) {
|
|
4163
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
3062
4164
|
const def = this._sheetDefs.find((s)=>s.name === name);
|
|
3063
4165
|
if (def) {
|
|
3064
4166
|
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
@@ -3073,8 +4175,23 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3073
4175
|
if (this._pivotTables.length > 0) {
|
|
3074
4176
|
this._updatePivotTableFiles();
|
|
3075
4177
|
}
|
|
4178
|
+
// Update tables (sets table rel IDs for tableParts)
|
|
4179
|
+
this._updateTableFiles();
|
|
4180
|
+
// Update worksheets to align tableParts with relationship IDs
|
|
4181
|
+
for (const [name, worksheet] of this._sheets){
|
|
4182
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
4183
|
+
const def = this._sheetDefs.find((s)=>s.name === name);
|
|
4184
|
+
if (def) {
|
|
4185
|
+
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
4186
|
+
if (rel) {
|
|
4187
|
+
const sheetPath = `xl/${rel.target}`;
|
|
4188
|
+
writeZipText(this._files, sheetPath, worksheet.toXml());
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
3076
4193
|
}
|
|
3077
|
-
_updateWorkbookXml() {
|
|
4194
|
+
_updateWorkbookXml(pivotCacheRelIds) {
|
|
3078
4195
|
const sheetNodes = this._sheetDefs.map((def)=>createElement('sheet', {
|
|
3079
4196
|
name: def.name,
|
|
3080
4197
|
sheetId: String(def.sheetId),
|
|
@@ -3086,9 +4203,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3086
4203
|
];
|
|
3087
4204
|
// Add pivot caches if any
|
|
3088
4205
|
if (this._pivotCaches.length > 0) {
|
|
3089
|
-
const pivotCacheNodes = this._pivotCaches.map((cache
|
|
3090
|
-
|
|
3091
|
-
|
|
4206
|
+
const pivotCacheNodes = this._pivotCaches.map((cache)=>{
|
|
4207
|
+
const cacheRelId = pivotCacheRelIds.get(cache.cacheId);
|
|
4208
|
+
if (!cacheRelId) {
|
|
4209
|
+
throw new Error(`Missing pivot cache relationship ID for cache ${cache.cacheId}`);
|
|
4210
|
+
}
|
|
3092
4211
|
return createElement('pivotCache', {
|
|
3093
4212
|
cacheId: String(cache.cacheId),
|
|
3094
4213
|
'r:id': cacheRelId
|
|
@@ -3105,20 +4224,38 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3105
4224
|
])}`;
|
|
3106
4225
|
writeZipText(this._files, 'xl/workbook.xml', xml);
|
|
3107
4226
|
}
|
|
3108
|
-
_updateRelationshipsXml() {
|
|
4227
|
+
_updateRelationshipsXml(relNodes) {
|
|
4228
|
+
const relsNode = createElement('Relationships', {
|
|
4229
|
+
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
4230
|
+
}, relNodes);
|
|
4231
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
4232
|
+
relsNode
|
|
4233
|
+
])}`;
|
|
4234
|
+
writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
|
|
4235
|
+
}
|
|
4236
|
+
_buildRelationshipInfo() {
|
|
3109
4237
|
const relNodes = this._relationships.map((rel)=>createElement('Relationship', {
|
|
3110
4238
|
Id: rel.id,
|
|
3111
4239
|
Type: rel.type,
|
|
3112
4240
|
Target: rel.target
|
|
3113
4241
|
}, []));
|
|
3114
|
-
|
|
4242
|
+
const reservedRelIds = new Set(relNodes.map((node)=>getAttr(node, 'Id') || '').filter(Boolean));
|
|
3115
4243
|
let nextRelId = Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
|
|
4244
|
+
const allocateRelId = ()=>{
|
|
4245
|
+
while(reservedRelIds.has(`rId${nextRelId}`)){
|
|
4246
|
+
nextRelId++;
|
|
4247
|
+
}
|
|
4248
|
+
const id = `rId${nextRelId}`;
|
|
4249
|
+
nextRelId++;
|
|
4250
|
+
reservedRelIds.add(id);
|
|
4251
|
+
return id;
|
|
4252
|
+
};
|
|
3116
4253
|
// Add shared strings relationship if needed
|
|
3117
4254
|
if (this._sharedStrings.count > 0) {
|
|
3118
4255
|
const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
|
|
3119
4256
|
if (!hasSharedStrings) {
|
|
3120
4257
|
relNodes.push(createElement('Relationship', {
|
|
3121
|
-
Id:
|
|
4258
|
+
Id: allocateRelId(),
|
|
3122
4259
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
|
|
3123
4260
|
Target: 'sharedStrings.xml'
|
|
3124
4261
|
}, []));
|
|
@@ -3128,26 +4265,26 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3128
4265
|
const hasStyles = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles');
|
|
3129
4266
|
if (!hasStyles) {
|
|
3130
4267
|
relNodes.push(createElement('Relationship', {
|
|
3131
|
-
Id:
|
|
4268
|
+
Id: allocateRelId(),
|
|
3132
4269
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
|
|
3133
4270
|
Target: 'styles.xml'
|
|
3134
4271
|
}, []));
|
|
3135
4272
|
}
|
|
3136
4273
|
// Add pivot cache relationships
|
|
3137
|
-
|
|
4274
|
+
const pivotCacheRelIds = new Map();
|
|
4275
|
+
for (const cache of this._pivotCaches){
|
|
4276
|
+
const id = allocateRelId();
|
|
4277
|
+
pivotCacheRelIds.set(cache.cacheId, id);
|
|
3138
4278
|
relNodes.push(createElement('Relationship', {
|
|
3139
|
-
Id:
|
|
4279
|
+
Id: id,
|
|
3140
4280
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
3141
|
-
Target: `pivotCache/pivotCacheDefinition${
|
|
4281
|
+
Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`
|
|
3142
4282
|
}, []));
|
|
3143
4283
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
relsNode
|
|
3149
|
-
])}`;
|
|
3150
|
-
writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
|
|
4284
|
+
return {
|
|
4285
|
+
relNodes,
|
|
4286
|
+
pivotCacheRelIds
|
|
4287
|
+
};
|
|
3151
4288
|
}
|
|
3152
4289
|
_updateContentTypes() {
|
|
3153
4290
|
const types = [
|
|
@@ -3186,23 +4323,67 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3186
4323
|
}
|
|
3187
4324
|
}
|
|
3188
4325
|
// Add pivot cache definitions and records
|
|
3189
|
-
for(
|
|
4326
|
+
for (const cache of this._pivotCaches){
|
|
3190
4327
|
types.push(createElement('Override', {
|
|
3191
|
-
PartName: `/xl/pivotCache/pivotCacheDefinition${
|
|
4328
|
+
PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
|
|
3192
4329
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
|
|
3193
4330
|
}, []));
|
|
3194
4331
|
types.push(createElement('Override', {
|
|
3195
|
-
PartName: `/xl/pivotCache/pivotCacheRecords${
|
|
4332
|
+
PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,
|
|
3196
4333
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
|
|
3197
4334
|
}, []));
|
|
3198
4335
|
}
|
|
3199
4336
|
// Add pivot tables
|
|
3200
|
-
for(
|
|
4337
|
+
for (const pivotTable of this._pivotTables){
|
|
3201
4338
|
types.push(createElement('Override', {
|
|
3202
|
-
PartName: `/xl/pivotTables/pivotTable${
|
|
4339
|
+
PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,
|
|
3203
4340
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
|
|
3204
4341
|
}, []));
|
|
3205
4342
|
}
|
|
4343
|
+
// Add tables
|
|
4344
|
+
let tableIndex = 1;
|
|
4345
|
+
for (const def of this._sheetDefs){
|
|
4346
|
+
const worksheet = this._sheets.get(def.name);
|
|
4347
|
+
if (worksheet) {
|
|
4348
|
+
for(let i = 0; i < worksheet.tables.length; i++){
|
|
4349
|
+
types.push(createElement('Override', {
|
|
4350
|
+
PartName: `/xl/tables/table${tableIndex}.xml`,
|
|
4351
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml'
|
|
4352
|
+
}, []));
|
|
4353
|
+
tableIndex++;
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
|
|
4358
|
+
const existingKeys = new Set(types.map((t)=>{
|
|
4359
|
+
if ('Default' in t) {
|
|
4360
|
+
const a = t[':@'];
|
|
4361
|
+
return `Default:${a?.['@_Extension'] || ''}`;
|
|
4362
|
+
}
|
|
4363
|
+
if ('Override' in t) {
|
|
4364
|
+
const a = t[':@'];
|
|
4365
|
+
return `Override:${a?.['@_PartName'] || ''}`;
|
|
4366
|
+
}
|
|
4367
|
+
return '';
|
|
4368
|
+
}).filter(Boolean));
|
|
4369
|
+
if (existingTypesXml) {
|
|
4370
|
+
const parsed = parseXml(existingTypesXml);
|
|
4371
|
+
const typesElement = findElement(parsed, 'Types');
|
|
4372
|
+
if (typesElement) {
|
|
4373
|
+
const existingNodes = getChildren(typesElement, 'Types');
|
|
4374
|
+
for (const node of existingNodes){
|
|
4375
|
+
if ('Default' in node || 'Override' in node) {
|
|
4376
|
+
const type = 'Default' in node ? 'Default' : 'Override';
|
|
4377
|
+
const attrs = node[':@'];
|
|
4378
|
+
const key = type === 'Default' ? `Default:${attrs?.['@_Extension'] || ''}` : `Override:${attrs?.['@_PartName'] || ''}`;
|
|
4379
|
+
if (!existingKeys.has(key)) {
|
|
4380
|
+
types.push(node);
|
|
4381
|
+
existingKeys.add(key);
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
3206
4387
|
const typesNode = createElement('Types', {
|
|
3207
4388
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types'
|
|
3208
4389
|
}, types);
|
|
@@ -3242,22 +4423,21 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3242
4423
|
// Generate pivot cache files
|
|
3243
4424
|
for(let i = 0; i < this._pivotCaches.length; i++){
|
|
3244
4425
|
const cache = this._pivotCaches[i];
|
|
3245
|
-
const cacheIdx = i + 1;
|
|
3246
4426
|
// Pivot cache definition
|
|
3247
|
-
const definitionPath = `xl/pivotCache/pivotCacheDefinition${
|
|
4427
|
+
const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;
|
|
3248
4428
|
writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
|
|
3249
4429
|
// Pivot cache records
|
|
3250
|
-
const recordsPath = `xl/pivotCache/pivotCacheRecords${
|
|
4430
|
+
const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;
|
|
3251
4431
|
writeZipText(this._files, recordsPath, cache.toRecordsXml());
|
|
3252
4432
|
// Pivot cache definition relationships (link to records)
|
|
3253
|
-
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${
|
|
4433
|
+
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;
|
|
3254
4434
|
const cacheRels = createElement('Relationships', {
|
|
3255
4435
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
3256
4436
|
}, [
|
|
3257
4437
|
createElement('Relationship', {
|
|
3258
4438
|
Id: 'rId1',
|
|
3259
4439
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
3260
|
-
Target: `pivotCacheRecords${
|
|
4440
|
+
Target: `pivotCacheRecords${cache.fileIndex}.xml`
|
|
3261
4441
|
}, [])
|
|
3262
4442
|
]);
|
|
3263
4443
|
writeZipText(this._files, cacheRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
@@ -3267,12 +4447,12 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3267
4447
|
// Generate pivot table files
|
|
3268
4448
|
for(let i = 0; i < this._pivotTables.length; i++){
|
|
3269
4449
|
const pivotTable = this._pivotTables[i];
|
|
3270
|
-
const ptIdx =
|
|
4450
|
+
const ptIdx = pivotTable.index;
|
|
3271
4451
|
// Pivot table definition
|
|
3272
4452
|
const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
|
|
3273
4453
|
writeZipText(this._files, ptPath, pivotTable.toXml());
|
|
3274
4454
|
// Pivot table relationships (link to cache definition)
|
|
3275
|
-
const cacheIdx =
|
|
4455
|
+
const cacheIdx = pivotTable.cacheFileIndex;
|
|
3276
4456
|
const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
|
|
3277
4457
|
const ptRels = createElement('Relationships', {
|
|
3278
4458
|
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
@@ -3296,13 +4476,47 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3296
4476
|
// Extract sheet file name from target path
|
|
3297
4477
|
const sheetFileName = rel.target.split('/').pop();
|
|
3298
4478
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
3299
|
-
const
|
|
3300
|
-
|
|
3301
|
-
|
|
4479
|
+
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
4480
|
+
let relNodes = [];
|
|
4481
|
+
let nextRelId = 1;
|
|
4482
|
+
const reservedRelIds = new Set();
|
|
4483
|
+
if (existingRelsXml) {
|
|
4484
|
+
const parsed = parseXml(existingRelsXml);
|
|
4485
|
+
const relsElement = findElement(parsed, 'Relationships');
|
|
4486
|
+
if (relsElement) {
|
|
4487
|
+
const existingRelNodes = getChildren(relsElement, 'Relationships');
|
|
4488
|
+
for (const relNode of existingRelNodes){
|
|
4489
|
+
if ('Relationship' in relNode) {
|
|
4490
|
+
relNodes.push(relNode);
|
|
4491
|
+
const id = getAttr(relNode, 'Id');
|
|
4492
|
+
if (id) {
|
|
4493
|
+
reservedRelIds.add(id);
|
|
4494
|
+
const idNum = parseInt(id.replace('rId', ''), 10);
|
|
4495
|
+
if (idNum >= nextRelId) {
|
|
4496
|
+
nextRelId = idNum + 1;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
const allocateRelId = ()=>{
|
|
4504
|
+
while(reservedRelIds.has(`rId${nextRelId}`)){
|
|
4505
|
+
nextRelId++;
|
|
4506
|
+
}
|
|
4507
|
+
const id = `rId${nextRelId}`;
|
|
4508
|
+
nextRelId++;
|
|
4509
|
+
reservedRelIds.add(id);
|
|
4510
|
+
return id;
|
|
4511
|
+
};
|
|
4512
|
+
for (const pt of pivotTables){
|
|
4513
|
+
const target = `../pivotTables/pivotTable${pt.index}.xml`;
|
|
4514
|
+
const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' && getAttr(node, 'Target') === target);
|
|
4515
|
+
if (existing) continue;
|
|
3302
4516
|
relNodes.push(createElement('Relationship', {
|
|
3303
|
-
Id:
|
|
4517
|
+
Id: allocateRelId(),
|
|
3304
4518
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
|
|
3305
|
-
Target:
|
|
4519
|
+
Target: target
|
|
3306
4520
|
}, []));
|
|
3307
4521
|
}
|
|
3308
4522
|
const sheetRels = createElement('Relationships', {
|
|
@@ -3313,6 +4527,109 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3313
4527
|
])}`);
|
|
3314
4528
|
}
|
|
3315
4529
|
}
|
|
4530
|
+
/**
|
|
4531
|
+
* Generate all table related files
|
|
4532
|
+
*/ _updateTableFiles() {
|
|
4533
|
+
// Collect all tables with their global indices
|
|
4534
|
+
let globalTableIndex = 1;
|
|
4535
|
+
const sheetTables = new Map();
|
|
4536
|
+
for (const def of this._sheetDefs){
|
|
4537
|
+
const worksheet = this._sheets.get(def.name);
|
|
4538
|
+
if (!worksheet) continue;
|
|
4539
|
+
const tables = worksheet.tables;
|
|
4540
|
+
if (tables.length === 0) continue;
|
|
4541
|
+
const tableInfos = [];
|
|
4542
|
+
for (const table of tables){
|
|
4543
|
+
tableInfos.push({
|
|
4544
|
+
table,
|
|
4545
|
+
globalIndex: globalTableIndex
|
|
4546
|
+
});
|
|
4547
|
+
globalTableIndex++;
|
|
4548
|
+
}
|
|
4549
|
+
sheetTables.set(def.name, tableInfos);
|
|
4550
|
+
}
|
|
4551
|
+
// Generate table files
|
|
4552
|
+
for (const [, tableInfos] of sheetTables){
|
|
4553
|
+
for (const { table, globalIndex } of tableInfos){
|
|
4554
|
+
const tablePath = `xl/tables/table${globalIndex}.xml`;
|
|
4555
|
+
writeZipText(this._files, tablePath, table.toXml());
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
// Generate worksheet relationships for tables
|
|
4559
|
+
for (const [sheetName, tableInfos] of sheetTables){
|
|
4560
|
+
const def = this._sheetDefs.find((s)=>s.name === sheetName);
|
|
4561
|
+
if (!def) continue;
|
|
4562
|
+
const rel = this._relationships.find((r)=>r.id === def.rId);
|
|
4563
|
+
if (!rel) continue;
|
|
4564
|
+
// Extract sheet file name from target path
|
|
4565
|
+
const sheetFileName = rel.target.split('/').pop();
|
|
4566
|
+
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
4567
|
+
// Check if there are already pivot table relationships for this sheet
|
|
4568
|
+
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
4569
|
+
let nextRelId = 1;
|
|
4570
|
+
const relNodes = [];
|
|
4571
|
+
const reservedRelIds = new Set();
|
|
4572
|
+
if (existingRelsXml) {
|
|
4573
|
+
// Parse existing rels and find max rId
|
|
4574
|
+
const parsed = parseXml(existingRelsXml);
|
|
4575
|
+
const relsElement = findElement(parsed, 'Relationships');
|
|
4576
|
+
if (relsElement) {
|
|
4577
|
+
const existingRelNodes = getChildren(relsElement, 'Relationships');
|
|
4578
|
+
for (const relNode of existingRelNodes){
|
|
4579
|
+
if ('Relationship' in relNode) {
|
|
4580
|
+
relNodes.push(relNode);
|
|
4581
|
+
const id = getAttr(relNode, 'Id');
|
|
4582
|
+
if (id) {
|
|
4583
|
+
reservedRelIds.add(id);
|
|
4584
|
+
const idNum = parseInt(id.replace('rId', ''), 10);
|
|
4585
|
+
if (idNum >= nextRelId) {
|
|
4586
|
+
nextRelId = idNum + 1;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
const allocateRelId = ()=>{
|
|
4594
|
+
while(reservedRelIds.has(`rId${nextRelId}`)){
|
|
4595
|
+
nextRelId++;
|
|
4596
|
+
}
|
|
4597
|
+
const id = `rId${nextRelId}`;
|
|
4598
|
+
nextRelId++;
|
|
4599
|
+
reservedRelIds.add(id);
|
|
4600
|
+
return id;
|
|
4601
|
+
};
|
|
4602
|
+
// Add table relationships
|
|
4603
|
+
const tableRelIds = [];
|
|
4604
|
+
for (const { globalIndex } of tableInfos){
|
|
4605
|
+
const target = `../tables/table${globalIndex}.xml`;
|
|
4606
|
+
const existing = relNodes.some((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
|
|
4607
|
+
if (existing) {
|
|
4608
|
+
const existingRel = relNodes.find((node)=>getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' && getAttr(node, 'Target') === target);
|
|
4609
|
+
const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
|
|
4610
|
+
tableRelIds.push(existingId ?? allocateRelId());
|
|
4611
|
+
continue;
|
|
4612
|
+
}
|
|
4613
|
+
const id = allocateRelId();
|
|
4614
|
+
tableRelIds.push(id);
|
|
4615
|
+
relNodes.push(createElement('Relationship', {
|
|
4616
|
+
Id: id,
|
|
4617
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
|
|
4618
|
+
Target: target
|
|
4619
|
+
}, []));
|
|
4620
|
+
}
|
|
4621
|
+
const worksheet = this._sheets.get(sheetName);
|
|
4622
|
+
if (worksheet) {
|
|
4623
|
+
worksheet.setTableRelIds(tableRelIds);
|
|
4624
|
+
}
|
|
4625
|
+
const sheetRels = createElement('Relationships', {
|
|
4626
|
+
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
|
|
4627
|
+
}, relNodes);
|
|
4628
|
+
writeZipText(this._files, sheetRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
|
|
4629
|
+
sheetRels
|
|
4630
|
+
])}`);
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
3316
4633
|
}
|
|
3317
4634
|
|
|
3318
|
-
export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Workbook, Worksheet, parseAddress, parseRange, toAddress, toRange };
|
|
4635
|
+
export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Table, Workbook, Worksheet, parseAddress, parseRange, toAddress, toRange };
|