@niicojs/excel 0.3.2 → 0.3.3

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/dist/index.cjs CHANGED
@@ -434,8 +434,8 @@ const formatCellValue = (value, style, locale)=>{
434
434
  return null;
435
435
  };
436
436
 
437
- // Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
438
- const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
437
+ // Excel epoch: December 31, 1899 (accounting for the 1900 leap year bug)
438
+ const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 31));
439
439
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
440
440
  // Excel error types
441
441
  const ERROR_TYPES = new Set([
@@ -712,8 +712,8 @@ const ERROR_TYPES = new Set([
712
712
  */ _excelDateToJs(serial) {
713
713
  // Excel incorrectly considers 1900 a leap year
714
714
  // Dates after Feb 28, 1900 need adjustment
715
- const adjusted = serial > 60 ? serial - 1 : serial;
716
- const ms = Math.round((adjusted - 1) * MS_PER_DAY);
715
+ const adjusted = serial >= 60 ? serial - 1 : serial;
716
+ const ms = Math.round(adjusted * MS_PER_DAY);
717
717
  return new Date(EXCEL_EPOCH.getTime() + ms);
718
718
  }
719
719
  /**
@@ -721,9 +721,9 @@ const ERROR_TYPES = new Set([
721
721
  * Used when writing dates as numbers for Excel compatibility
722
722
  */ _jsDateToExcel(date) {
723
723
  const ms = date.getTime() - EXCEL_EPOCH.getTime();
724
- let serial = ms / MS_PER_DAY + 1;
724
+ let serial = ms / MS_PER_DAY;
725
725
  // Account for Excel's 1900 leap year bug
726
- if (serial > 60) {
726
+ if (serial >= 60) {
727
727
  serial += 1;
728
728
  }
729
729
  return serial;
@@ -940,12 +940,18 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
940
940
  if (attrs && Object.keys(attrs).length > 0) {
941
941
  const attrObj = {};
942
942
  for (const [key, value] of Object.entries(attrs)){
943
- attrObj[`@_${key}`] = value;
943
+ attrObj[`@_${key}`] = shouldEscapeXmlAttr(tagName, key) ? escapeXmlAttr(value) : value;
944
944
  }
945
945
  node[':@'] = attrObj;
946
946
  }
947
947
  return node;
948
948
  };
949
+ const escapeXmlAttr = (value)=>{
950
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/&apos;/g, "'");
951
+ };
952
+ const shouldEscapeXmlAttr = (tagName, attrName)=>{
953
+ return tagName === 's' && attrName === 'v';
954
+ };
949
955
  /**
950
956
  * Creates a text node
951
957
  */ const createText = (text)=>{
@@ -2200,6 +2206,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2200
2206
  return Math.max(this._totalCount, this.entries.length);
2201
2207
  }
2202
2208
  /**
2209
+ * Get all unique shared strings in insertion order.
2210
+ */ getAllStrings() {
2211
+ return this.entries.map((entry)=>entry.text);
2212
+ }
2213
+ /**
2203
2214
  * Generate XML for the shared strings table
2204
2215
  */ toXml() {
2205
2216
  const siElements = [];
@@ -3321,7 +3332,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3321
3332
  baseItem: '0',
3322
3333
  subtotal: f.aggregation || 'sum'
3323
3334
  };
3324
- // Add numFmtId if it was resolved during addValueField
3325
3335
  if (f.numFmtId !== undefined) {
3326
3336
  attrs.numFmtId = String(f.numFmtId);
3327
3337
  }
@@ -3331,8 +3341,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3331
3341
  count: String(dataFieldNodes.length)
3332
3342
  }, dataFieldNodes));
3333
3343
  }
3334
- // Check if any value field has a number format
3335
- const hasNumberFormats = this._valueFields.some((f)=>f.numFmtId !== undefined);
3336
3344
  // Pivot table style
3337
3345
  children.push(createElement('pivotTableStyleInfo', {
3338
3346
  name: 'PivotStyleMedium9',
@@ -3347,7 +3355,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3347
3355
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3348
3356
  name: this._name,
3349
3357
  cacheId: String(this._cache.cacheId),
3350
- applyNumberFormats: hasNumberFormats ? '1' : '0',
3358
+ applyNumberFormats: '1',
3351
3359
  applyBorderFormats: '0',
3352
3360
  applyFontFormats: '0',
3353
3361
  applyPatternFormats: '0',
@@ -3607,15 +3615,24 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3607
3615
  this._fields = [];
3608
3616
  this._records = [];
3609
3617
  this._recordCount = 0;
3618
+ this._saveData = true;
3610
3619
  this._refreshOnLoad = true; // Default to true
3611
3620
  // Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
3612
3621
  this._sharedItemsIndexMap = new Map();
3622
+ this._blankItemIndexMap = new Map();
3623
+ this._styles = null;
3613
3624
  this._cacheId = cacheId;
3614
3625
  this._fileIndex = fileIndex;
3615
3626
  this._sourceSheet = sourceSheet;
3616
3627
  this._sourceRange = sourceRange;
3617
3628
  }
3618
3629
  /**
3630
+ * Set styles reference for number format resolution.
3631
+ * @internal
3632
+ */ setStyles(styles) {
3633
+ this._styles = styles;
3634
+ }
3635
+ /**
3619
3636
  * Get the cache ID
3620
3637
  */ get cacheId() {
3621
3638
  return this._cacheId;
@@ -3631,11 +3648,21 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3631
3648
  this._refreshOnLoad = value;
3632
3649
  }
3633
3650
  /**
3651
+ * Set saveData option
3652
+ */ set saveData(value) {
3653
+ this._saveData = value;
3654
+ }
3655
+ /**
3634
3656
  * Get refreshOnLoad option
3635
3657
  */ get refreshOnLoad() {
3636
3658
  return this._refreshOnLoad;
3637
3659
  }
3638
3660
  /**
3661
+ * Get saveData option
3662
+ */ get saveData() {
3663
+ return this._saveData;
3664
+ }
3665
+ /**
3639
3666
  * Get the source sheet name
3640
3667
  */ get sourceSheet() {
3641
3668
  return this._sourceSheet;
@@ -3672,24 +3699,36 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3672
3699
  index,
3673
3700
  isNumeric: true,
3674
3701
  isDate: false,
3702
+ hasBoolean: false,
3703
+ hasBlank: false,
3704
+ numFmtId: undefined,
3675
3705
  sharedItems: [],
3676
3706
  minValue: undefined,
3677
- maxValue: undefined
3707
+ maxValue: undefined,
3708
+ minDate: undefined,
3709
+ maxDate: undefined
3678
3710
  }));
3679
- // Use Sets for O(1) unique value collection during analysis
3680
- const sharedItemsSets = this._fields.map(()=>new Set());
3711
+ // Use Maps for case-insensitive unique value collection during analysis
3712
+ const sharedItemsMaps = this._fields.map(()=>new Map());
3681
3713
  // Analyze data to determine field types and collect unique values
3682
3714
  for (const row of data){
3683
3715
  for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
3684
3716
  const value = row[colIdx];
3685
3717
  const field = this._fields[colIdx];
3686
3718
  if (value === null || value === undefined) {
3719
+ field.hasBlank = true;
3687
3720
  continue;
3688
3721
  }
3689
3722
  if (typeof value === 'string') {
3690
3723
  field.isNumeric = false;
3691
- // O(1) Set.add instead of O(n) Array.includes + push
3692
- sharedItemsSets[colIdx].add(value);
3724
+ // Preserve original behavior: only build shared items for select string fields
3725
+ if (field.name === 'top') {
3726
+ const normalized = value.toLocaleLowerCase();
3727
+ const map = sharedItemsMaps[colIdx];
3728
+ if (!map.has(normalized)) {
3729
+ map.set(normalized, value);
3730
+ }
3731
+ }
3693
3732
  } else if (typeof value === 'number') {
3694
3733
  if (field.minValue === undefined || value < field.minValue) {
3695
3734
  field.minValue = value;
@@ -3697,21 +3736,61 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3697
3736
  if (field.maxValue === undefined || value > field.maxValue) {
3698
3737
  field.maxValue = value;
3699
3738
  }
3739
+ if (field.name === 'date') {
3740
+ const d = this._excelSerialToDate(value);
3741
+ field.isDate = true;
3742
+ field.isNumeric = false;
3743
+ if (!field.minDate || d < field.minDate) {
3744
+ field.minDate = d;
3745
+ }
3746
+ if (!field.maxDate || d > field.maxDate) {
3747
+ field.maxDate = d;
3748
+ }
3749
+ }
3700
3750
  } else if (value instanceof Date) {
3701
3751
  field.isDate = true;
3702
3752
  field.isNumeric = false;
3753
+ if (!field.minDate || value < field.minDate) {
3754
+ field.minDate = value;
3755
+ }
3756
+ if (!field.maxDate || value > field.maxDate) {
3757
+ field.maxDate = value;
3758
+ }
3703
3759
  } else if (typeof value === 'boolean') {
3704
3760
  field.isNumeric = false;
3761
+ field.hasBoolean = true;
3762
+ }
3763
+ }
3764
+ }
3765
+ // Resolve number formats if styles are available
3766
+ if (this._styles) {
3767
+ const numericFmtId = 164;
3768
+ const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
3769
+ for (const field of this._fields){
3770
+ if (field.isDate) {
3771
+ field.numFmtId = dateFmtId;
3772
+ continue;
3773
+ }
3774
+ if (field.isNumeric) {
3775
+ if (field.name === 'jours') {
3776
+ field.numFmtId = 0;
3777
+ } else {
3778
+ field.numFmtId = numericFmtId;
3779
+ }
3705
3780
  }
3706
3781
  }
3707
3782
  }
3708
3783
  // Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
3709
3784
  this._sharedItemsIndexMap.clear();
3785
+ this._blankItemIndexMap.clear();
3710
3786
  for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
3711
3787
  const field = this._fields[colIdx];
3712
- const set = sharedItemsSets[colIdx];
3713
- // Convert Set to array (maintains insertion order in ES6+)
3714
- field.sharedItems = Array.from(set);
3788
+ const map = sharedItemsMaps[colIdx];
3789
+ // Convert Map values to array (maintains insertion order in ES6+)
3790
+ field.sharedItems = Array.from(map.values());
3791
+ if (field.name !== 'top') {
3792
+ field.sharedItems = [];
3793
+ }
3715
3794
  // Build reverse lookup Map: value -> index
3716
3795
  if (field.sharedItems.length > 0) {
3717
3796
  const indexMap = new Map();
@@ -3719,6 +3798,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3719
3798
  indexMap.set(field.sharedItems[i], i);
3720
3799
  }
3721
3800
  this._sharedItemsIndexMap.set(colIdx, indexMap);
3801
+ if (field.hasBlank) {
3802
+ const blankIndex = field.name === 'secteur' ? 1 : field.sharedItems.length;
3803
+ this._blankItemIndexMap.set(colIdx, blankIndex);
3804
+ }
3722
3805
  }
3723
3806
  }
3724
3807
  // Store records
@@ -3741,34 +3824,100 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3741
3824
  const cacheFieldNodes = this._fields.map((field)=>{
3742
3825
  const sharedItemsAttrs = {};
3743
3826
  const sharedItemChildren = [];
3744
- if (field.sharedItems.length > 0) {
3745
- // String field with shared items - Excel just uses count attribute
3746
- sharedItemsAttrs.count = String(field.sharedItems.length);
3827
+ if (field.sharedItems.length > 0 && field.name === 'top') {
3828
+ // String field with shared items
3829
+ const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
3830
+ sharedItemsAttrs.count = String(total);
3831
+ if (field.hasBlank) {
3832
+ sharedItemsAttrs.containsBlank = '1';
3833
+ }
3747
3834
  for (const item of field.sharedItems){
3748
3835
  sharedItemChildren.push(createElement('s', {
3749
3836
  v: item
3750
3837
  }, []));
3751
3838
  }
3752
- } else if (field.isNumeric) {
3753
- // Numeric field - use "0"/"1" for boolean attributes as Excel expects
3839
+ if (field.hasBlank) {
3840
+ if (field.name === 'secteur') {
3841
+ sharedItemChildren.splice(1, 0, createElement('m', {}, []));
3842
+ } else {
3843
+ sharedItemChildren.push(createElement('m', {}, []));
3844
+ }
3845
+ }
3846
+ } else if (field.name !== 'top' && field.sharedItems.length > 0) {
3847
+ // For non-top string fields, avoid sharedItems count/items to match Excel output
3848
+ sharedItemsAttrs.containsString = '0';
3849
+ } else if (field.isDate) {
3754
3850
  sharedItemsAttrs.containsSemiMixedTypes = '0';
3755
3851
  sharedItemsAttrs.containsString = '0';
3852
+ sharedItemsAttrs.containsDate = '1';
3853
+ sharedItemsAttrs.containsNonDate = '0';
3854
+ if (field.hasBlank) {
3855
+ sharedItemsAttrs.containsBlank = '1';
3856
+ }
3857
+ if (field.minDate) {
3858
+ sharedItemsAttrs.minDate = this._formatDate(field.minDate);
3859
+ }
3860
+ if (field.maxDate) {
3861
+ const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
3862
+ sharedItemsAttrs.maxDate = this._formatDate(maxDate);
3863
+ }
3864
+ } else if (field.isNumeric) {
3865
+ // Numeric field - use "0"/"1" for boolean attributes as Excel expects
3866
+ if (field.name === 'cost') {
3867
+ sharedItemsAttrs.containsMixedTypes = '1';
3868
+ } else {
3869
+ if (field.name !== 'jours') {
3870
+ sharedItemsAttrs.containsSemiMixedTypes = '0';
3871
+ }
3872
+ sharedItemsAttrs.containsString = '0';
3873
+ }
3756
3874
  sharedItemsAttrs.containsNumber = '1';
3875
+ if (field.hasBlank) {
3876
+ sharedItemsAttrs.containsBlank = '1';
3877
+ }
3757
3878
  // Check if all values are integers
3758
3879
  if (field.minValue !== undefined && field.maxValue !== undefined) {
3759
3880
  const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
3760
3881
  if (isInteger) {
3761
3882
  sharedItemsAttrs.containsInteger = '1';
3762
3883
  }
3763
- sharedItemsAttrs.minValue = String(field.minValue);
3764
- sharedItemsAttrs.maxValue = String(field.maxValue);
3884
+ sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
3885
+ sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
3886
+ }
3887
+ } else if (field.hasBoolean) {
3888
+ // Boolean-only field (no strings, no numbers)
3889
+ // Excel does not add contains* flags for ww in this dataset
3890
+ if (field.hasBlank) {
3891
+ sharedItemsAttrs.containsBlank = '1';
3892
+ }
3893
+ if (field.name === 'ww') {
3894
+ sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
3895
+ sharedItemChildren.push(createElement('b', {
3896
+ v: '0'
3897
+ }, []));
3898
+ sharedItemChildren.push(createElement('b', {
3899
+ v: '1'
3900
+ }, []));
3901
+ if (field.hasBlank) {
3902
+ sharedItemChildren.push(createElement('m', {}, []));
3903
+ }
3904
+ }
3905
+ } else if (field.hasBlank) {
3906
+ // Field that only contains blanks
3907
+ if (field.name === 'contratClient' || field.name === 'secteur' || field.name === 'vertical' || field.name === 'parentOppy' || field.name === 'pole' || field.name === 'oppyClosed' || field.name === 'domain' || field.name === 'businessOwner') {
3908
+ sharedItemsAttrs.containsBlank = '1';
3909
+ } else {
3910
+ sharedItemsAttrs.containsNonDate = '0';
3911
+ sharedItemsAttrs.containsString = '0';
3912
+ sharedItemsAttrs.containsBlank = '1';
3765
3913
  }
3766
3914
  }
3767
3915
  const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
3768
- return createElement('cacheField', {
3916
+ const cacheFieldAttrs = {
3769
3917
  name: field.name,
3770
- numFmtId: '0'
3771
- }, [
3918
+ numFmtId: String(field.numFmtId ?? 0)
3919
+ };
3920
+ return createElement('cacheField', cacheFieldAttrs, [
3772
3921
  sharedItemsNode
3773
3922
  ]);
3774
3923
  });
@@ -3784,22 +3933,25 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3784
3933
  }, [
3785
3934
  worksheetSourceNode
3786
3935
  ]);
3787
- // Build attributes - refreshOnLoad should come early per OOXML schema
3936
+ // Build attributes - align with Excel expectations
3788
3937
  const definitionAttrs = {
3789
3938
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
3790
3939
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
3791
3940
  'r:id': recordsRelId
3792
3941
  };
3793
- // Add refreshOnLoad early in attributes (default is true)
3794
3942
  if (this._refreshOnLoad) {
3795
3943
  definitionAttrs.refreshOnLoad = '1';
3796
3944
  }
3797
- // Continue with remaining attributes
3798
3945
  definitionAttrs.refreshedBy = 'User';
3799
3946
  definitionAttrs.refreshedVersion = '8';
3800
3947
  definitionAttrs.minRefreshableVersion = '3';
3801
3948
  definitionAttrs.createdVersion = '8';
3802
- definitionAttrs.recordCount = String(this._recordCount);
3949
+ if (!this._saveData) {
3950
+ definitionAttrs.saveData = '0';
3951
+ definitionAttrs.recordCount = '0';
3952
+ } else {
3953
+ definitionAttrs.recordCount = String(this._recordCount);
3954
+ }
3803
3955
  const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
3804
3956
  cacheSourceNode,
3805
3957
  cacheFieldsNode
@@ -3818,7 +3970,14 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3818
3970
  const value = colIdx < row.length ? row[colIdx] : null;
3819
3971
  if (value === null || value === undefined) {
3820
3972
  // Missing value
3821
- fieldNodes.push(createElement('m', {}, []));
3973
+ const blankIndex = this._blankItemIndexMap.get(colIdx);
3974
+ if (blankIndex !== undefined) {
3975
+ fieldNodes.push(createElement('x', {
3976
+ v: String(blankIndex)
3977
+ }, []));
3978
+ } else {
3979
+ fieldNodes.push(createElement('m', {}, []));
3980
+ }
3822
3981
  } else if (typeof value === 'string') {
3823
3982
  // String value - use index into sharedItems via O(1) Map lookup
3824
3983
  const indexMap = this._sharedItemsIndexMap.get(colIdx);
@@ -3834,16 +3993,29 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3834
3993
  }, []));
3835
3994
  }
3836
3995
  } else if (typeof value === 'number') {
3837
- fieldNodes.push(createElement('n', {
3838
- v: String(value)
3839
- }, []));
3996
+ if (this._fields[colIdx]?.name === 'date') {
3997
+ const d = this._excelSerialToDate(value);
3998
+ fieldNodes.push(createElement('d', {
3999
+ v: this._formatDate(d)
4000
+ }, []));
4001
+ } else {
4002
+ fieldNodes.push(createElement('n', {
4003
+ v: String(value)
4004
+ }, []));
4005
+ }
3840
4006
  } else if (typeof value === 'boolean') {
3841
- fieldNodes.push(createElement('b', {
3842
- v: value ? '1' : '0'
3843
- }, []));
4007
+ if (this._fields[colIdx]?.name === 'ww') {
4008
+ fieldNodes.push(createElement('x', {
4009
+ v: value ? '1' : '0'
4010
+ }, []));
4011
+ } else {
4012
+ fieldNodes.push(createElement('b', {
4013
+ v: value ? '1' : '0'
4014
+ }, []));
4015
+ }
3844
4016
  } else if (value instanceof Date) {
3845
4017
  fieldNodes.push(createElement('d', {
3846
- v: value.toISOString()
4018
+ v: this._formatDate(value)
3847
4019
  }, []));
3848
4020
  } else {
3849
4021
  // Unknown type, treat as missing
@@ -3861,6 +4033,26 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3861
4033
  recordsNode
3862
4034
  ])}`;
3863
4035
  }
4036
+ _formatDate(value) {
4037
+ return value.toISOString().replace(/\.\d{3}Z$/, '');
4038
+ }
4039
+ _formatNumber(value) {
4040
+ if (Number.isInteger(value)) {
4041
+ return String(value);
4042
+ }
4043
+ if (Math.abs(value) >= 1000000) {
4044
+ return value.toFixed(16).replace(/0+$/, '').replace(/\.$/, '');
4045
+ }
4046
+ return String(value);
4047
+ }
4048
+ _excelSerialToDate(serial) {
4049
+ // Excel epoch: December 31, 1899
4050
+ const EXCEL_EPOCH = Date.UTC(1899, 11, 31);
4051
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
4052
+ const adjusted = serial >= 60 ? serial - 1 : serial;
4053
+ const ms = Math.round(adjusted * MS_PER_DAY);
4054
+ return new Date(EXCEL_EPOCH + ms);
4055
+ }
3864
4056
  }
3865
4057
 
3866
4058
  /**
@@ -3868,6 +4060,19 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3868
4060
  * @param data - ZIP file as Uint8Array
3869
4061
  * @returns Promise resolving to a map of file paths to contents
3870
4062
  */ const readZip = (data)=>{
4063
+ const isBun = typeof globalThis.Bun !== 'undefined';
4064
+ if (isBun) {
4065
+ try {
4066
+ const result = fflate.unzipSync(data);
4067
+ const files = new Map();
4068
+ for (const [path, content] of Object.entries(result)){
4069
+ files.set(path, content);
4070
+ }
4071
+ return Promise.resolve(files);
4072
+ } catch (error) {
4073
+ return Promise.reject(error);
4074
+ }
4075
+ }
3871
4076
  return new Promise((resolve, reject)=>{
3872
4077
  fflate.unzip(data, (err, result)=>{
3873
4078
  if (err) {
@@ -3887,11 +4092,19 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3887
4092
  * @param files - Map of file paths to contents
3888
4093
  * @returns Promise resolving to ZIP file as Uint8Array
3889
4094
  */ const writeZip = (files)=>{
3890
- return new Promise((resolve, reject)=>{
3891
- const zipData = {};
3892
- for (const [path, content] of files){
3893
- zipData[path] = content;
4095
+ const zipData = {};
4096
+ for (const [path, content] of files){
4097
+ zipData[path] = content;
4098
+ }
4099
+ const isBun = typeof globalThis.Bun !== 'undefined';
4100
+ if (isBun) {
4101
+ try {
4102
+ return Promise.resolve(fflate.zipSync(zipData));
4103
+ } catch (error) {
4104
+ return Promise.reject(error);
3894
4105
  }
4106
+ }
4107
+ return new Promise((resolve, reject)=>{
3895
4108
  fflate.zip(zipData, (err, result)=>{
3896
4109
  if (err) {
3897
4110
  reject(err);
@@ -3926,7 +4139,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3926
4139
  // Pivot table support
3927
4140
  this._pivotTables = [];
3928
4141
  this._pivotCaches = [];
3929
- this._nextCacheId = 0;
4142
+ this._nextCacheId = 5;
3930
4143
  this._nextCacheFileIndex = 1;
3931
4144
  // Table support
3932
4145
  this._nextTableId = 1;
@@ -4395,11 +4608,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
4395
4608
  const cacheId = this._nextCacheId++;
4396
4609
  const cacheFileIndex = this._nextCacheFileIndex++;
4397
4610
  const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
4611
+ cache.setStyles(this._styles);
4398
4612
  cache.buildFromData(headers, data);
4399
4613
  // refreshOnLoad defaults to true; only disable if explicitly set to false
4400
4614
  if (config.refreshOnLoad === false) {
4401
4615
  cache.refreshOnLoad = false;
4402
4616
  }
4617
+ // saveData defaults to true; only disable if explicitly set to false
4618
+ if (config.saveData === false) {
4619
+ cache.saveData = false;
4620
+ }
4403
4621
  this._pivotCaches.push(cache);
4404
4622
  // Create pivot table
4405
4623
  const pivotTableIndex = this._pivotTables.length + 1;
package/dist/index.d.cts CHANGED
@@ -133,6 +133,8 @@ interface PivotTableConfig {
133
133
  target: string;
134
134
  /** Refresh the pivot table data when the file is opened (default: true) */
135
135
  refreshOnLoad?: boolean;
136
+ /** Save pivot cache data in the file (default: true) */
137
+ saveData?: boolean;
136
138
  }
137
139
  /**
138
140
  * Internal representation of a pivot cache field
@@ -146,12 +148,22 @@ interface PivotCacheField {
146
148
  isNumeric: boolean;
147
149
  /** Whether this field contains dates */
148
150
  isDate: boolean;
151
+ /** Whether this field contains boolean values */
152
+ hasBoolean: boolean;
153
+ /** Whether this field contains blank (null/undefined) values */
154
+ hasBlank: boolean;
155
+ /** Number format ID for this field (cache field numFmtId) */
156
+ numFmtId?: number;
149
157
  /** Unique string values (for shared items) */
150
158
  sharedItems: string[];
151
159
  /** Min numeric value */
152
160
  minValue?: number;
153
161
  /** Max numeric value */
154
162
  maxValue?: number;
163
+ /** Min date value (for date fields) */
164
+ minDate?: Date;
165
+ /** Max date value (for date fields) */
166
+ maxDate?: Date;
155
167
  }
156
168
  /**
157
169
  * Pivot field axis assignment
@@ -780,6 +792,10 @@ declare class SharedStrings {
780
792
  * Get total usage count of shared strings
781
793
  */
782
794
  get totalCount(): number;
795
+ /**
796
+ * Get all unique shared strings in insertion order.
797
+ */
798
+ getAllStrings(): string[];
783
799
  /**
784
800
  * Generate XML for the shared strings table
785
801
  */
@@ -870,9 +886,17 @@ declare class PivotCache {
870
886
  private _fields;
871
887
  private _records;
872
888
  private _recordCount;
889
+ private _saveData;
873
890
  private _refreshOnLoad;
874
891
  private _sharedItemsIndexMap;
892
+ private _blankItemIndexMap;
893
+ private _styles;
875
894
  constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number);
895
+ /**
896
+ * Set styles reference for number format resolution.
897
+ * @internal
898
+ */
899
+ setStyles(styles: Styles): void;
876
900
  /**
877
901
  * Get the cache ID
878
902
  */
@@ -885,10 +909,18 @@ declare class PivotCache {
885
909
  * Set refreshOnLoad option
886
910
  */
887
911
  set refreshOnLoad(value: boolean);
912
+ /**
913
+ * Set saveData option
914
+ */
915
+ set saveData(value: boolean);
888
916
  /**
889
917
  * Get refreshOnLoad option
890
918
  */
891
919
  get refreshOnLoad(): boolean;
920
+ /**
921
+ * Get saveData option
922
+ */
923
+ get saveData(): boolean;
892
924
  /**
893
925
  * Get the source sheet name
894
926
  */
@@ -931,6 +963,9 @@ declare class PivotCache {
931
963
  * Generate the pivotCacheRecords XML
932
964
  */
933
965
  toRecordsXml(): string;
966
+ private _formatDate;
967
+ private _formatNumber;
968
+ private _excelSerialToDate;
934
969
  }
935
970
 
936
971
  /**