sheetra 1.0.1 → 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
@@ -404,6 +404,593 @@ class Worksheet {
404
404
  }
405
405
  }
406
406
 
407
+ /**
408
+ * Minimal ZIP file creator for XLSX generation (no external dependencies)
409
+ * Uses STORE method (no compression) for simplicity and compatibility
410
+ */
411
+ class ZipWriter {
412
+ constructor() {
413
+ this.files = [];
414
+ this.encoder = new TextEncoder();
415
+ }
416
+ addFile(name, content) {
417
+ this.files.push({
418
+ name,
419
+ data: this.encoder.encode(content)
420
+ });
421
+ }
422
+ generate() {
423
+ const localFiles = [];
424
+ const centralDirectory = [];
425
+ let offset = 0;
426
+ for (const file of this.files) {
427
+ const localHeader = this.createLocalFileHeader(file.name, file.data);
428
+ localFiles.push(localHeader);
429
+ localFiles.push(file.data);
430
+ const centralHeader = this.createCentralDirectoryHeader(file.name, file.data, offset);
431
+ centralDirectory.push(centralHeader);
432
+ offset += localHeader.length + file.data.length;
433
+ }
434
+ const centralDirStart = offset;
435
+ let centralDirSize = 0;
436
+ for (const header of centralDirectory) {
437
+ centralDirSize += header.length;
438
+ }
439
+ const endOfCentralDir = this.createEndOfCentralDirectory(this.files.length, centralDirSize, centralDirStart);
440
+ // Combine all parts
441
+ const totalSize = offset + centralDirSize + endOfCentralDir.length;
442
+ const result = new Uint8Array(totalSize);
443
+ let pos = 0;
444
+ for (const local of localFiles) {
445
+ result.set(local, pos);
446
+ pos += local.length;
447
+ }
448
+ for (const central of centralDirectory) {
449
+ result.set(central, pos);
450
+ pos += central.length;
451
+ }
452
+ result.set(endOfCentralDir, pos);
453
+ return result;
454
+ }
455
+ createLocalFileHeader(name, data) {
456
+ const nameBytes = this.encoder.encode(name);
457
+ const crc = this.crc32(data);
458
+ const header = new Uint8Array(30 + nameBytes.length);
459
+ const view = new DataView(header.buffer);
460
+ view.setUint32(0, 0x04034b50, true); // Local file header signature
461
+ view.setUint16(4, 20, true); // Version needed to extract
462
+ view.setUint16(6, 0, true); // General purpose bit flag
463
+ view.setUint16(8, 0, true); // Compression method (STORE)
464
+ view.setUint16(10, 0, true); // File last modification time
465
+ view.setUint16(12, 0, true); // File last modification date
466
+ view.setUint32(14, crc, true); // CRC-32
467
+ view.setUint32(18, data.length, true); // Compressed size
468
+ view.setUint32(22, data.length, true); // Uncompressed size
469
+ view.setUint16(26, nameBytes.length, true); // File name length
470
+ view.setUint16(28, 0, true); // Extra field length
471
+ header.set(nameBytes, 30);
472
+ return header;
473
+ }
474
+ createCentralDirectoryHeader(name, data, localHeaderOffset) {
475
+ const nameBytes = this.encoder.encode(name);
476
+ const crc = this.crc32(data);
477
+ const header = new Uint8Array(46 + nameBytes.length);
478
+ const view = new DataView(header.buffer);
479
+ view.setUint32(0, 0x02014b50, true); // Central directory file header signature
480
+ view.setUint16(4, 20, true); // Version made by
481
+ view.setUint16(6, 20, true); // Version needed to extract
482
+ view.setUint16(8, 0, true); // General purpose bit flag
483
+ view.setUint16(10, 0, true); // Compression method (STORE)
484
+ view.setUint16(12, 0, true); // File last modification time
485
+ view.setUint16(14, 0, true); // File last modification date
486
+ view.setUint32(16, crc, true); // CRC-32
487
+ view.setUint32(20, data.length, true); // Compressed size
488
+ view.setUint32(24, data.length, true); // Uncompressed size
489
+ view.setUint16(28, nameBytes.length, true); // File name length
490
+ view.setUint16(30, 0, true); // Extra field length
491
+ view.setUint16(32, 0, true); // File comment length
492
+ view.setUint16(34, 0, true); // Disk number start
493
+ view.setUint16(36, 0, true); // Internal file attributes
494
+ view.setUint32(38, 0, true); // External file attributes
495
+ view.setUint32(42, localHeaderOffset, true); // Relative offset of local header
496
+ header.set(nameBytes, 46);
497
+ return header;
498
+ }
499
+ createEndOfCentralDirectory(numFiles, centralDirSize, centralDirOffset) {
500
+ const header = new Uint8Array(22);
501
+ const view = new DataView(header.buffer);
502
+ view.setUint32(0, 0x06054b50, true); // End of central directory signature
503
+ view.setUint16(4, 0, true); // Number of this disk
504
+ view.setUint16(6, 0, true); // Disk where central directory starts
505
+ view.setUint16(8, numFiles, true); // Number of central directory records on this disk
506
+ view.setUint16(10, numFiles, true); // Total number of central directory records
507
+ view.setUint32(12, centralDirSize, true); // Size of central directory
508
+ view.setUint32(16, centralDirOffset, true); // Offset of start of central directory
509
+ view.setUint16(20, 0, true); // Comment length
510
+ return header;
511
+ }
512
+ crc32(data) {
513
+ let crc = 0xFFFFFFFF;
514
+ for (let i = 0; i < data.length; i++) {
515
+ crc ^= data[i];
516
+ for (let j = 0; j < 8; j++) {
517
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
518
+ }
519
+ }
520
+ return (crc ^ 0xFFFFFFFF) >>> 0;
521
+ }
522
+ }
523
+ class ExcelWriter {
524
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
525
+ static async write(workbook, _options) {
526
+ const zip = new ZipWriter();
527
+ const sheets = workbook['sheets'];
528
+ const sheetNames = sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`);
529
+ // Collect all styles from sheets
530
+ const styleRegistry = this.collectStyles(sheets);
531
+ // Add required XLSX files
532
+ zip.addFile('[Content_Types].xml', this.generateContentTypes(sheetNames));
533
+ zip.addFile('_rels/.rels', this.generateRels());
534
+ zip.addFile('xl/workbook.xml', this.generateWorkbook(sheetNames));
535
+ zip.addFile('xl/_rels/workbook.xml.rels', this.generateWorkbookRels(sheetNames));
536
+ zip.addFile('xl/styles.xml', this.generateStyles(styleRegistry));
537
+ zip.addFile('xl/sharedStrings.xml', this.generateSharedStrings(sheets));
538
+ // Add each worksheet
539
+ sheets.forEach((sheet, index) => {
540
+ zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet, styleRegistry));
541
+ });
542
+ const buffer = zip.generate();
543
+ return new Blob([buffer.buffer], {
544
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
545
+ });
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
+ }
652
+ static escapeXml(str) {
653
+ if (typeof str !== 'string')
654
+ return String(str !== null && str !== void 0 ? str : '');
655
+ return str
656
+ .replace(/&/g, '&amp;')
657
+ .replace(/</g, '&lt;')
658
+ .replace(/>/g, '&gt;')
659
+ .replace(/"/g, '&quot;')
660
+ .replace(/'/g, '&apos;');
661
+ }
662
+ static generateContentTypes(sheetNames) {
663
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
664
+ xml += '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
665
+ xml += '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
666
+ xml += '<Default Extension="xml" ContentType="application/xml"/>';
667
+ xml += '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
668
+ xml += '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
669
+ xml += '<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>';
670
+ sheetNames.forEach((_, index) => {
671
+ xml += `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
672
+ });
673
+ xml += '</Types>';
674
+ return xml;
675
+ }
676
+ static generateRels() {
677
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
678
+ xml += '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
679
+ xml += '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>';
680
+ xml += '</Relationships>';
681
+ return xml;
682
+ }
683
+ static generateWorkbook(sheetNames) {
684
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
685
+ xml += '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
686
+ xml += '<sheets>';
687
+ sheetNames.forEach((name, index) => {
688
+ xml += `<sheet name="${this.escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`;
689
+ });
690
+ xml += '</sheets>';
691
+ xml += '</workbook>';
692
+ return xml;
693
+ }
694
+ static generateWorkbookRels(sheetNames) {
695
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
696
+ xml += '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
697
+ sheetNames.forEach((_, index) => {
698
+ xml += `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`;
699
+ });
700
+ xml += `<Relationship Id="rId${sheetNames.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
701
+ xml += `<Relationship Id="rId${sheetNames.length + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`;
702
+ xml += '</Relationships>';
703
+ return xml;
704
+ }
705
+ static generateStyles(registry) {
706
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
707
+ xml += '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
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)
776
+ xml += '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>';
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>';
825
+ xml += '</styleSheet>';
826
+ return xml;
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
+ }
849
+ static generateSharedStrings(sheets) {
850
+ const strings = [];
851
+ sheets.forEach((sheet) => {
852
+ const rows = sheet.getRows();
853
+ rows.forEach((row) => {
854
+ row.getCells().forEach((cell) => {
855
+ const cellData = cell.toData();
856
+ if (typeof cellData.value === 'string' && !cellData.formula) {
857
+ if (!strings.includes(cellData.value)) {
858
+ strings.push(cellData.value);
859
+ }
860
+ }
861
+ });
862
+ });
863
+ });
864
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
865
+ xml += `<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${strings.length}" uniqueCount="${strings.length}">`;
866
+ strings.forEach(str => {
867
+ xml += `<si><t>${this.escapeXml(str)}</t></si>`;
868
+ });
869
+ xml += '</sst>';
870
+ return xml;
871
+ }
872
+ static generateWorksheet(sheet, styleRegistry) {
873
+ const rows = sheet.getRows();
874
+ const columns = sheet.getColumns();
875
+ const allStrings = this.collectAllStrings(sheet);
876
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
877
+ xml += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
878
+ // Add sheet dimension
879
+ const dimension = this.getSheetDimension(rows);
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
+ }
898
+ xml += '<sheetData>';
899
+ rows.forEach((row, rowIndex) => {
900
+ const cells = row.getCells();
901
+ if (cells.length > 0) {
902
+ xml += `<row r="${rowIndex + 1}">`;
903
+ cells.forEach((cell, colIndex) => {
904
+ const cellData = cell.toData();
905
+ const cellRef = this.columnToLetter(colIndex + 1) + (rowIndex + 1);
906
+ const styleIndex = this.getStyleIndex(cellData.style, styleRegistry);
907
+ const styleAttr = styleIndex > 0 ? ` s="${styleIndex}"` : '';
908
+ if (cellData.formula) {
909
+ xml += `<c r="${cellRef}"${styleAttr}><f>${this.escapeXml(cellData.formula)}</f></c>`;
910
+ }
911
+ else if (cellData.value !== null && cellData.value !== undefined && cellData.value !== '') {
912
+ if (typeof cellData.value === 'number') {
913
+ xml += `<c r="${cellRef}"${styleAttr}><v>${cellData.value}</v></c>`;
914
+ }
915
+ else if (typeof cellData.value === 'boolean') {
916
+ xml += `<c r="${cellRef}"${styleAttr} t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
917
+ }
918
+ else if (cellData.type === 'date') {
919
+ const excelDate = DateFormatter.toExcelDate(cellData.value);
920
+ xml += `<c r="${cellRef}"${styleAttr}><v>${excelDate}</v></c>`;
921
+ }
922
+ else {
923
+ // String value - use shared string index
924
+ const stringIndex = allStrings.indexOf(String(cellData.value));
925
+ if (stringIndex >= 0) {
926
+ xml += `<c r="${cellRef}"${styleAttr} t="s"><v>${stringIndex}</v></c>`;
927
+ }
928
+ else {
929
+ // Inline string as fallback
930
+ xml += `<c r="${cellRef}"${styleAttr} t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
931
+ }
932
+ }
933
+ }
934
+ else if (styleIndex > 0) {
935
+ // Empty cell with style
936
+ xml += `<c r="${cellRef}"${styleAttr}/>`;
937
+ }
938
+ });
939
+ xml += '</row>';
940
+ }
941
+ });
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
+ }
954
+ xml += '</worksheet>';
955
+ return xml;
956
+ }
957
+ static collectAllStrings(sheet) {
958
+ const strings = [];
959
+ const rows = sheet.getRows();
960
+ rows.forEach((row) => {
961
+ row.getCells().forEach((cell) => {
962
+ const cellData = cell.toData();
963
+ if (typeof cellData.value === 'string' && !cellData.formula) {
964
+ if (!strings.includes(cellData.value)) {
965
+ strings.push(cellData.value);
966
+ }
967
+ }
968
+ });
969
+ });
970
+ return strings;
971
+ }
972
+ static getSheetDimension(rows) {
973
+ if (rows.length === 0)
974
+ return 'A1';
975
+ let maxCol = 1;
976
+ rows.forEach(row => {
977
+ const cellCount = row.getCells().length;
978
+ if (cellCount > maxCol)
979
+ maxCol = cellCount;
980
+ });
981
+ return `A1:${this.columnToLetter(maxCol)}${rows.length}`;
982
+ }
983
+ static columnToLetter(column) {
984
+ let letters = '';
985
+ while (column > 0) {
986
+ const temp = (column - 1) % 26;
987
+ letters = String.fromCharCode(temp + 65) + letters;
988
+ column = Math.floor((column - temp - 1) / 26);
989
+ }
990
+ return letters || 'A';
991
+ }
992
+ }
993
+
407
994
  class CSVWriter {
408
995
  static async write(workbook, _options) {
409
996
  const sheets = workbook['sheets'];
@@ -475,102 +1062,6 @@ class JSONWriter {
475
1062
  }
476
1063
  }
477
1064
 
478
- class ExcelWriter {
479
- static async write(workbook, _options) {
480
- const data = this.generateExcelData(workbook);
481
- const buffer = this.createExcelFile(data);
482
- const uint8Buffer = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
483
- const arrayBuffer = uint8Buffer.slice().buffer;
484
- return new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
485
- }
486
- static generateExcelData(workbook) {
487
- const sheets = workbook['sheets'];
488
- return {
489
- Sheets: sheets.reduce((acc, sheet, index) => {
490
- const sheetName = sheet.getName() || `Sheet${index + 1}`;
491
- acc[sheetName] = this.generateSheetData(sheet);
492
- return acc;
493
- }, {}),
494
- SheetNames: sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`)
495
- };
496
- }
497
- static generateSheetData(sheet) {
498
- const rows = sheet.getRows();
499
- const data = [];
500
- rows.forEach((row) => {
501
- const rowData = [];
502
- row.getCells().forEach((cell) => {
503
- const cellData = cell.toData();
504
- if (cellData.formula) {
505
- rowData.push({ f: cellData.formula });
506
- }
507
- else if (cellData.type === 'date' &&
508
- (typeof cellData.value === 'string' ||
509
- typeof cellData.value === 'number' ||
510
- cellData.value instanceof Date)) {
511
- rowData.push(DateFormatter.toExcelDate(cellData.value));
512
- }
513
- else {
514
- rowData.push(cellData.value);
515
- }
516
- });
517
- data.push(rowData);
518
- });
519
- return {
520
- '!ref': this.getSheetRange(data),
521
- '!rows': rows.map(row => ({
522
- hpt: row['height'],
523
- hidden: row['hidden'],
524
- outlineLevel: row['outlineLevel'],
525
- collapsed: row['collapsed']
526
- })),
527
- '!cols': sheet.getColumns().map(col => ({
528
- wch: col['width'],
529
- hidden: col['hidden'],
530
- outlineLevel: col['outlineLevel'],
531
- collapsed: col['collapsed']
532
- })),
533
- '!merges': sheet['mergeCells'],
534
- '!freeze': sheet['freezePane']
535
- };
536
- }
537
- static getSheetRange(data) {
538
- if (data.length === 0)
539
- return 'A1:A1';
540
- const lastRow = data.length;
541
- const lastCol = Math.max(...data.map(row => row.length));
542
- return `A1:${this.columnToLetter(lastCol)}${lastRow}`;
543
- }
544
- static columnToLetter(column) {
545
- let letters = '';
546
- while (column > 0) {
547
- const temp = (column - 1) % 26;
548
- letters = String.fromCharCode(temp + 65) + letters;
549
- column = (column - temp - 1) / 26;
550
- }
551
- return letters || 'A';
552
- }
553
- static createExcelFile(data) {
554
- // This is a simplified Excel file generator
555
- // In a real implementation, you'd generate proper XLSX binary format
556
- // For now, we'll return a simple XML-based structure
557
- const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
558
- const workbook = this.generateWorkbookXML(data);
559
- const encoder = new TextEncoder();
560
- return encoder.encode(xmlHeader + workbook);
561
- }
562
- static generateWorkbookXML(data) {
563
- let xml = '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
564
- xml += '<sheets>';
565
- data.SheetNames.forEach((name, index) => {
566
- xml += `<sheet name="${name}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`;
567
- });
568
- xml += '</sheets>';
569
- xml += '</workbook>';
570
- return xml;
571
- }
572
- }
573
-
574
1065
  class Workbook {
575
1066
  constructor(data) {
576
1067
  this.sheets = [];
@@ -828,6 +1319,55 @@ class ExportBuilder {
828
1319
  });
829
1320
  return this;
830
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
+ }
831
1371
  autoSizeColumns() {
832
1372
  const rows = this.currentSheet.getRows();
833
1373
  const maxLengths = [];
@@ -952,6 +1492,28 @@ class SheetBuilder {
952
1492
  });
953
1493
  return this;
954
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
+ }
955
1517
  /**
956
1518
  * Add a title row
957
1519
  * @param title Title text