sheetra 1.0.2 → 1.0.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.js CHANGED
@@ -526,22 +526,129 @@ class ExcelWriter {
526
526
  const zip = new ZipWriter();
527
527
  const sheets = workbook['sheets'];
528
528
  const sheetNames = sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`);
529
+ // Collect all styles from sheets
530
+ const styleRegistry = this.collectStyles(sheets);
529
531
  // Add required XLSX files
530
532
  zip.addFile('[Content_Types].xml', this.generateContentTypes(sheetNames));
531
533
  zip.addFile('_rels/.rels', this.generateRels());
532
534
  zip.addFile('xl/workbook.xml', this.generateWorkbook(sheetNames));
533
535
  zip.addFile('xl/_rels/workbook.xml.rels', this.generateWorkbookRels(sheetNames));
534
- zip.addFile('xl/styles.xml', this.generateStyles());
536
+ zip.addFile('xl/styles.xml', this.generateStyles(styleRegistry));
535
537
  zip.addFile('xl/sharedStrings.xml', this.generateSharedStrings(sheets));
536
538
  // Add each worksheet
537
539
  sheets.forEach((sheet, index) => {
538
- zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet));
540
+ zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet, styleRegistry));
539
541
  });
540
542
  const buffer = zip.generate();
541
543
  return new Blob([buffer.buffer], {
542
544
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
543
545
  });
544
546
  }
547
+ /**
548
+ * Collect all unique styles from sheets
549
+ */
550
+ static collectStyles(sheets) {
551
+ const registry = {
552
+ fonts: new Map(),
553
+ fills: new Map(),
554
+ borders: new Map(),
555
+ cellXfs: new Map()
556
+ };
557
+ // Add default font (Calibri 11)
558
+ const defaultFont = this.serializeFont({});
559
+ registry.fonts.set(defaultFont, 0);
560
+ // Add required fills (none and gray125)
561
+ registry.fills.set('none', 0);
562
+ registry.fills.set('gray125', 1);
563
+ // Add default border
564
+ registry.borders.set('none', 0);
565
+ // Add default cellXf (no style)
566
+ registry.cellXfs.set('default', 0);
567
+ // Collect styles from all cells
568
+ sheets.forEach((sheet) => {
569
+ const rows = sheet.getRows();
570
+ rows.forEach((row) => {
571
+ row.getCells().forEach((cell) => {
572
+ const cellData = cell.toData();
573
+ if (cellData.style) {
574
+ this.registerStyle(cellData.style, registry);
575
+ }
576
+ });
577
+ });
578
+ });
579
+ return registry;
580
+ }
581
+ /**
582
+ * Register a style and get its cellXf index
583
+ */
584
+ static registerStyle(style, registry) {
585
+ var _a, _b, _c, _d;
586
+ // Serialize and register font
587
+ const fontKey = this.serializeFont(style);
588
+ if (!registry.fonts.has(fontKey)) {
589
+ registry.fonts.set(fontKey, registry.fonts.size);
590
+ }
591
+ // Serialize and register fill
592
+ const fillKey = this.serializeFill(style);
593
+ if (fillKey !== 'none' && !registry.fills.has(fillKey)) {
594
+ registry.fills.set(fillKey, registry.fills.size);
595
+ }
596
+ // Serialize and register border
597
+ const borderKey = this.serializeBorder(style);
598
+ if (borderKey !== 'none' && !registry.borders.has(borderKey)) {
599
+ registry.borders.set(borderKey, registry.borders.size);
600
+ }
601
+ // Create cellXf key
602
+ const fontIndex = (_a = registry.fonts.get(fontKey)) !== null && _a !== void 0 ? _a : 0;
603
+ const fillIndex = (_b = registry.fills.get(fillKey)) !== null && _b !== void 0 ? _b : 0;
604
+ const borderIndex = (_c = registry.borders.get(borderKey)) !== null && _c !== void 0 ? _c : 0;
605
+ let alignment = this.serializeAlignment(style);
606
+ if (!alignment)
607
+ alignment = '{}';
608
+ const xfKey = `f${fontIndex}:l${fillIndex}:b${borderIndex}:a${alignment}`;
609
+ if (!registry.cellXfs.has(xfKey)) {
610
+ registry.cellXfs.set(xfKey, registry.cellXfs.size);
611
+ }
612
+ return (_d = registry.cellXfs.get(xfKey)) !== null && _d !== void 0 ? _d : 0;
613
+ }
614
+ /**
615
+ * Get style index for a cell style
616
+ */
617
+ static getStyleIndex(style, registry) {
618
+ if (!style)
619
+ return 0;
620
+ return this.registerStyle(style, registry);
621
+ }
622
+ static serializeFont(style) {
623
+ var _a, _b, _c, _d, _e, _f;
624
+ const bold = style.bold || ((_a = style.font) === null || _a === void 0 ? void 0 : _a.bold);
625
+ const italic = style.italic || ((_b = style.font) === null || _b === void 0 ? void 0 : _b.italic);
626
+ const underline = style.underline || ((_c = style.font) === null || _c === void 0 ? void 0 : _c.underline);
627
+ const color = style.color || ((_d = style.font) === null || _d === void 0 ? void 0 : _d.color);
628
+ const size = style.fontSize || ((_e = style.font) === null || _e === void 0 ? void 0 : _e.size) || 11;
629
+ const name = style.fontFamily || ((_f = style.font) === null || _f === void 0 ? void 0 : _f.name) || 'Calibri';
630
+ return JSON.stringify({ bold, italic, underline, color, size, name });
631
+ }
632
+ static serializeFill(style) {
633
+ var _a;
634
+ const bgColor = style.backgroundColor || ((_a = style.fill) === null || _a === void 0 ? void 0 : _a.fgColor);
635
+ if (!bgColor)
636
+ return 'none';
637
+ return JSON.stringify({ bgColor });
638
+ }
639
+ static serializeBorder(style) {
640
+ if (!style.border && !style.borderAll)
641
+ return 'none';
642
+ return JSON.stringify({ border: style.border, borderAll: style.borderAll });
643
+ }
644
+ static serializeAlignment(style) {
645
+ const h = style.alignment;
646
+ const v = style.verticalAlignment;
647
+ const wrap = style.wrapText;
648
+ if (!h && !v && !wrap)
649
+ return '';
650
+ return JSON.stringify({ h, v, wrap });
651
+ }
545
652
  static escapeXml(str) {
546
653
  if (typeof str !== 'string')
547
654
  return String(str !== null && str !== void 0 ? str : '');
@@ -595,17 +702,150 @@ class ExcelWriter {
595
702
  xml += '</Relationships>';
596
703
  return xml;
597
704
  }
598
- static generateStyles() {
705
+ static generateStyles(registry) {
599
706
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
600
707
  xml += '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
601
- xml += '<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>';
602
- xml += '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>';
603
- xml += '<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>';
708
+ // Generate fonts
709
+ xml += `<fonts count="${registry.fonts.size}">`;
710
+ const fontEntries = Array.from(registry.fonts.entries()).sort((a, b) => a[1] - b[1]);
711
+ for (const [fontKey] of fontEntries) {
712
+ const font = JSON.parse(fontKey);
713
+ xml += '<font>';
714
+ if (font.bold)
715
+ xml += '<b/>';
716
+ if (font.italic)
717
+ xml += '<i/>';
718
+ if (font.underline)
719
+ xml += '<u/>';
720
+ xml += `<sz val="${font.size || 11}"/>`;
721
+ if (font.color) {
722
+ const colorHex = this.colorToARGB(font.color);
723
+ xml += `<color rgb="${colorHex}"/>`;
724
+ }
725
+ xml += `<name val="${font.name || 'Calibri'}"/>`;
726
+ xml += '</font>';
727
+ }
728
+ xml += '</fonts>';
729
+ // Generate fills
730
+ xml += `<fills count="${registry.fills.size}">`;
731
+ const fillEntries = Array.from(registry.fills.entries()).sort((a, b) => a[1] - b[1]);
732
+ for (const [fillKey] of fillEntries) {
733
+ if (fillKey === 'none') {
734
+ xml += '<fill><patternFill patternType="none"/></fill>';
735
+ }
736
+ else if (fillKey === 'gray125') {
737
+ xml += '<fill><patternFill patternType="gray125"/></fill>';
738
+ }
739
+ else {
740
+ const fill = JSON.parse(fillKey);
741
+ const colorHex = this.colorToARGB(fill.bgColor);
742
+ xml += `<fill><patternFill patternType="solid"><fgColor rgb="${colorHex}"/><bgColor indexed="64"/></patternFill></fill>`;
743
+ }
744
+ }
745
+ xml += '</fills>';
746
+ // Generate borders
747
+ xml += `<borders count="${registry.borders.size}">`;
748
+ const borderEntries = Array.from(registry.borders.entries()).sort((a, b) => a[1] - b[1]);
749
+ for (const [borderKey] of borderEntries) {
750
+ if (borderKey === 'none') {
751
+ xml += '<border><left/><right/><top/><bottom/><diagonal/></border>';
752
+ }
753
+ else {
754
+ const borderData = JSON.parse(borderKey);
755
+ xml += '<border>';
756
+ if (borderData.borderAll) {
757
+ const style = borderData.borderAll.style || 'thin';
758
+ const color = borderData.borderAll.color ? this.colorToARGB(borderData.borderAll.color) : 'FF000000';
759
+ xml += `<left style="${style}"><color rgb="${color}"/></left>`;
760
+ xml += `<right style="${style}"><color rgb="${color}"/></right>`;
761
+ xml += `<top style="${style}"><color rgb="${color}"/></top>`;
762
+ xml += `<bottom style="${style}"><color rgb="${color}"/></bottom>`;
763
+ }
764
+ else if (borderData.border) {
765
+ const b = borderData.border;
766
+ xml += this.generateBorderEdge('left', b.left);
767
+ xml += this.generateBorderEdge('right', b.right);
768
+ xml += this.generateBorderEdge('top', b.top);
769
+ xml += this.generateBorderEdge('bottom', b.bottom);
770
+ }
771
+ xml += '<diagonal/></border>';
772
+ }
773
+ }
774
+ xml += '</borders>';
775
+ // Generate cellStyleXfs (base styles)
604
776
  xml += '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>';
605
- xml += '<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>';
777
+ // Generate cellXfs (cell formats)
778
+ xml += `<cellXfs count="${registry.cellXfs.size}">`;
779
+ const xfEntries = Array.from(registry.cellXfs.entries()).sort((a, b) => a[1] - b[1]);
780
+ for (const [xfKey] of xfEntries) {
781
+ if (xfKey === 'default') {
782
+ xml += '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>';
783
+ }
784
+ else {
785
+ // Parse xf key: f{fontIndex}:l{fillIndex}:b{borderIndex}:a{alignment}
786
+ const parts = xfKey.split(':');
787
+ const fontId = parseInt(parts[0].substring(1)) || 0;
788
+ const fillId = parseInt(parts[1].substring(1)) || 0;
789
+ const borderId = parseInt(parts[2].substring(1)) || 0;
790
+ const alignmentJson = parts[3].substring(1);
791
+ let xf = `<xf numFmtId="0" fontId="${fontId}" fillId="${fillId}" borderId="${borderId}" xfId="0"`;
792
+ if (fontId > 0)
793
+ xf += ' applyFont="1"';
794
+ if (fillId > 0)
795
+ xf += ' applyFill="1"';
796
+ if (borderId > 0)
797
+ xf += ' applyBorder="1"';
798
+ let align = {};
799
+ if (alignmentJson && alignmentJson.trim().startsWith('{')) {
800
+ try {
801
+ align = JSON.parse(alignmentJson);
802
+ }
803
+ catch (e) {
804
+ align = {};
805
+ }
806
+ }
807
+ if (Object.keys(align).length > 0) {
808
+ xf += ' applyAlignment="1">';
809
+ xf += '<alignment';
810
+ if (align.h)
811
+ xf += ` horizontal="${align.h}"`;
812
+ if (align.v)
813
+ xf += ` vertical="${align.v === 'middle' ? 'center' : align.v}"`;
814
+ if (align.wrap)
815
+ xf += ' wrapText="1"';
816
+ xf += '/></xf>';
817
+ }
818
+ else {
819
+ xf += '/>';
820
+ }
821
+ xml += xf;
822
+ }
823
+ }
824
+ xml += '</cellXfs>';
606
825
  xml += '</styleSheet>';
607
826
  return xml;
608
827
  }
828
+ static generateBorderEdge(side, edge) {
829
+ if (!edge || !edge.style)
830
+ return `<${side}/>`;
831
+ const color = edge.color ? this.colorToARGB(edge.color) : 'FF000000';
832
+ return `<${side} style="${edge.style}"><color rgb="${color}"/></${side}>`;
833
+ }
834
+ static colorToARGB(color) {
835
+ if (!color)
836
+ return 'FF000000';
837
+ // Remove # if present
838
+ let hex = color.replace('#', '').toUpperCase();
839
+ // Add alpha if not present (6 chars -> 8 chars)
840
+ if (hex.length === 6) {
841
+ hex = 'FF' + hex;
842
+ }
843
+ // Handle 3 char hex
844
+ if (hex.length === 3) {
845
+ hex = 'FF' + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
846
+ }
847
+ return hex;
848
+ }
609
849
  static generateSharedStrings(sheets) {
610
850
  const strings = [];
611
851
  sheets.forEach((sheet) => {
@@ -629,14 +869,32 @@ class ExcelWriter {
629
869
  xml += '</sst>';
630
870
  return xml;
631
871
  }
632
- static generateWorksheet(sheet) {
872
+ static generateWorksheet(sheet, styleRegistry) {
633
873
  const rows = sheet.getRows();
874
+ const columns = sheet.getColumns();
634
875
  const allStrings = this.collectAllStrings(sheet);
635
876
  let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
636
877
  xml += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
637
878
  // Add sheet dimension
638
879
  const dimension = this.getSheetDimension(rows);
639
880
  xml += `<dimension ref="${dimension}"/>`;
881
+ // Add column widths if defined
882
+ if (columns.length > 0) {
883
+ xml += '<cols>';
884
+ columns.forEach((col, index) => {
885
+ const colData = col.toData();
886
+ if (colData.width !== undefined && colData.width > 0) {
887
+ // Excel width is in character units, convert from pixels: width / 7
888
+ const excelWidth = colData.width / 7;
889
+ xml += `<col min="${index + 1}" max="${index + 1}" width="${excelWidth}" customWidth="1"`;
890
+ if (colData.hidden) {
891
+ xml += ' hidden="1"';
892
+ }
893
+ xml += '/>';
894
+ }
895
+ });
896
+ xml += '</cols>';
897
+ }
640
898
  xml += '<sheetData>';
641
899
  rows.forEach((row, rowIndex) => {
642
900
  const cells = row.getCells();
@@ -645,37 +903,54 @@ class ExcelWriter {
645
903
  cells.forEach((cell, colIndex) => {
646
904
  const cellData = cell.toData();
647
905
  const cellRef = this.columnToLetter(colIndex + 1) + (rowIndex + 1);
906
+ const styleIndex = this.getStyleIndex(cellData.style, styleRegistry);
907
+ const styleAttr = styleIndex > 0 ? ` s="${styleIndex}"` : '';
648
908
  if (cellData.formula) {
649
- xml += `<c r="${cellRef}"><f>${this.escapeXml(cellData.formula)}</f></c>`;
909
+ xml += `<c r="${cellRef}"${styleAttr}><f>${this.escapeXml(cellData.formula)}</f></c>`;
650
910
  }
651
911
  else if (cellData.value !== null && cellData.value !== undefined && cellData.value !== '') {
652
912
  if (typeof cellData.value === 'number') {
653
- xml += `<c r="${cellRef}"><v>${cellData.value}</v></c>`;
913
+ xml += `<c r="${cellRef}"${styleAttr}><v>${cellData.value}</v></c>`;
654
914
  }
655
915
  else if (typeof cellData.value === 'boolean') {
656
- xml += `<c r="${cellRef}" t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
916
+ xml += `<c r="${cellRef}"${styleAttr} t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
657
917
  }
658
918
  else if (cellData.type === 'date') {
659
919
  const excelDate = DateFormatter.toExcelDate(cellData.value);
660
- xml += `<c r="${cellRef}"><v>${excelDate}</v></c>`;
920
+ xml += `<c r="${cellRef}"${styleAttr}><v>${excelDate}</v></c>`;
661
921
  }
662
922
  else {
663
923
  // String value - use shared string index
664
924
  const stringIndex = allStrings.indexOf(String(cellData.value));
665
925
  if (stringIndex >= 0) {
666
- xml += `<c r="${cellRef}" t="s"><v>${stringIndex}</v></c>`;
926
+ xml += `<c r="${cellRef}"${styleAttr} t="s"><v>${stringIndex}</v></c>`;
667
927
  }
668
928
  else {
669
929
  // Inline string as fallback
670
- xml += `<c r="${cellRef}" t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
930
+ xml += `<c r="${cellRef}"${styleAttr} t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
671
931
  }
672
932
  }
673
933
  }
934
+ else if (styleIndex > 0) {
935
+ // Empty cell with style
936
+ xml += `<c r="${cellRef}"${styleAttr}/>`;
937
+ }
674
938
  });
675
939
  xml += '</row>';
676
940
  }
677
941
  });
678
942
  xml += '</sheetData>';
943
+ // Add merged cells if any
944
+ const sheetData = sheet.toData();
945
+ if (sheetData.mergeCells && sheetData.mergeCells.length > 0) {
946
+ xml += '<mergeCells>';
947
+ sheetData.mergeCells.forEach(merge => {
948
+ const startRef = this.columnToLetter(merge.startCol + 1) + (merge.startRow + 1);
949
+ const endRef = this.columnToLetter(merge.endCol + 1) + (merge.endRow + 1);
950
+ xml += `<mergeCell ref="${startRef}:${endRef}"/>`;
951
+ });
952
+ xml += '</mergeCells>';
953
+ }
679
954
  xml += '</worksheet>';
680
955
  return xml;
681
956
  }
@@ -1044,6 +1319,55 @@ class ExportBuilder {
1044
1319
  });
1045
1320
  return this;
1046
1321
  }
1322
+ /**
1323
+ * Merge cells in the current worksheet
1324
+ * @param startRow Start row index (0-based)
1325
+ * @param startCol Start column index (0-based)
1326
+ * @param endRow End row index (0-based)
1327
+ * @param endCol End column index (0-based)
1328
+ */
1329
+ mergeCells(startRow, startCol, endRow, endCol) {
1330
+ this.currentSheet.mergeCells(startRow, startCol, endRow, endCol);
1331
+ return this;
1332
+ }
1333
+ /**
1334
+ * Set alignment for a specific cell
1335
+ * @param row Row index (0-based)
1336
+ * @param col Column index (0-based)
1337
+ * @param horizontal Horizontal alignment
1338
+ * @param vertical Vertical alignment (optional)
1339
+ */
1340
+ setAlignment(row, col, horizontal, vertical) {
1341
+ const rowObj = this.currentSheet.getRow(row);
1342
+ if (rowObj) {
1343
+ const cells = rowObj.getCells();
1344
+ if (cells[col]) {
1345
+ const style = StyleBuilder.create().align(horizontal);
1346
+ if (vertical) {
1347
+ style.verticalAlign(vertical);
1348
+ }
1349
+ cells[col].setStyle(style.build());
1350
+ }
1351
+ }
1352
+ return this;
1353
+ }
1354
+ /**
1355
+ * Set alignment for a range of cells
1356
+ * @param startRow Start row index (0-based)
1357
+ * @param startCol Start column index (0-based)
1358
+ * @param endRow End row index (0-based)
1359
+ * @param endCol End column index (0-based)
1360
+ * @param horizontal Horizontal alignment
1361
+ * @param vertical Vertical alignment (optional)
1362
+ */
1363
+ setRangeAlignment(startRow, startCol, endRow, endCol, horizontal, vertical) {
1364
+ for (let r = startRow; r <= endRow; r++) {
1365
+ for (let c = startCol; c <= endCol; c++) {
1366
+ this.setAlignment(r, c, horizontal, vertical);
1367
+ }
1368
+ }
1369
+ return this;
1370
+ }
1047
1371
  autoSizeColumns() {
1048
1372
  const rows = this.currentSheet.getRows();
1049
1373
  const maxLengths = [];
@@ -1168,6 +1492,28 @@ class SheetBuilder {
1168
1492
  });
1169
1493
  return this;
1170
1494
  }
1495
+ /**
1496
+ * Set the width of a specific column
1497
+ * @param colIndex Column index
1498
+ * @param width Width to set
1499
+ */
1500
+ setColumnWidth(colIndex, width) {
1501
+ const column = this.getOrCreateColumn(colIndex);
1502
+ column.setWidth(width);
1503
+ return this;
1504
+ }
1505
+ /**
1506
+ * Set the height of a specific row
1507
+ * @param rowIndex Row index
1508
+ * @param height Height to set
1509
+ */
1510
+ setRowHeight(rowIndex, height) {
1511
+ const row = this.worksheet.getRow(rowIndex);
1512
+ if (row) {
1513
+ row.setHeight(height);
1514
+ }
1515
+ return this;
1516
+ }
1171
1517
  /**
1172
1518
  * Add a title row
1173
1519
  * @param title Title text