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.esm.js CHANGED
@@ -402,6 +402,593 @@ class Worksheet {
402
402
  }
403
403
  }
404
404
 
405
+ /**
406
+ * Minimal ZIP file creator for XLSX generation (no external dependencies)
407
+ * Uses STORE method (no compression) for simplicity and compatibility
408
+ */
409
+ class ZipWriter {
410
+ constructor() {
411
+ this.files = [];
412
+ this.encoder = new TextEncoder();
413
+ }
414
+ addFile(name, content) {
415
+ this.files.push({
416
+ name,
417
+ data: this.encoder.encode(content)
418
+ });
419
+ }
420
+ generate() {
421
+ const localFiles = [];
422
+ const centralDirectory = [];
423
+ let offset = 0;
424
+ for (const file of this.files) {
425
+ const localHeader = this.createLocalFileHeader(file.name, file.data);
426
+ localFiles.push(localHeader);
427
+ localFiles.push(file.data);
428
+ const centralHeader = this.createCentralDirectoryHeader(file.name, file.data, offset);
429
+ centralDirectory.push(centralHeader);
430
+ offset += localHeader.length + file.data.length;
431
+ }
432
+ const centralDirStart = offset;
433
+ let centralDirSize = 0;
434
+ for (const header of centralDirectory) {
435
+ centralDirSize += header.length;
436
+ }
437
+ const endOfCentralDir = this.createEndOfCentralDirectory(this.files.length, centralDirSize, centralDirStart);
438
+ // Combine all parts
439
+ const totalSize = offset + centralDirSize + endOfCentralDir.length;
440
+ const result = new Uint8Array(totalSize);
441
+ let pos = 0;
442
+ for (const local of localFiles) {
443
+ result.set(local, pos);
444
+ pos += local.length;
445
+ }
446
+ for (const central of centralDirectory) {
447
+ result.set(central, pos);
448
+ pos += central.length;
449
+ }
450
+ result.set(endOfCentralDir, pos);
451
+ return result;
452
+ }
453
+ createLocalFileHeader(name, data) {
454
+ const nameBytes = this.encoder.encode(name);
455
+ const crc = this.crc32(data);
456
+ const header = new Uint8Array(30 + nameBytes.length);
457
+ const view = new DataView(header.buffer);
458
+ view.setUint32(0, 0x04034b50, true); // Local file header signature
459
+ view.setUint16(4, 20, true); // Version needed to extract
460
+ view.setUint16(6, 0, true); // General purpose bit flag
461
+ view.setUint16(8, 0, true); // Compression method (STORE)
462
+ view.setUint16(10, 0, true); // File last modification time
463
+ view.setUint16(12, 0, true); // File last modification date
464
+ view.setUint32(14, crc, true); // CRC-32
465
+ view.setUint32(18, data.length, true); // Compressed size
466
+ view.setUint32(22, data.length, true); // Uncompressed size
467
+ view.setUint16(26, nameBytes.length, true); // File name length
468
+ view.setUint16(28, 0, true); // Extra field length
469
+ header.set(nameBytes, 30);
470
+ return header;
471
+ }
472
+ createCentralDirectoryHeader(name, data, localHeaderOffset) {
473
+ const nameBytes = this.encoder.encode(name);
474
+ const crc = this.crc32(data);
475
+ const header = new Uint8Array(46 + nameBytes.length);
476
+ const view = new DataView(header.buffer);
477
+ view.setUint32(0, 0x02014b50, true); // Central directory file header signature
478
+ view.setUint16(4, 20, true); // Version made by
479
+ view.setUint16(6, 20, true); // Version needed to extract
480
+ view.setUint16(8, 0, true); // General purpose bit flag
481
+ view.setUint16(10, 0, true); // Compression method (STORE)
482
+ view.setUint16(12, 0, true); // File last modification time
483
+ view.setUint16(14, 0, true); // File last modification date
484
+ view.setUint32(16, crc, true); // CRC-32
485
+ view.setUint32(20, data.length, true); // Compressed size
486
+ view.setUint32(24, data.length, true); // Uncompressed size
487
+ view.setUint16(28, nameBytes.length, true); // File name length
488
+ view.setUint16(30, 0, true); // Extra field length
489
+ view.setUint16(32, 0, true); // File comment length
490
+ view.setUint16(34, 0, true); // Disk number start
491
+ view.setUint16(36, 0, true); // Internal file attributes
492
+ view.setUint32(38, 0, true); // External file attributes
493
+ view.setUint32(42, localHeaderOffset, true); // Relative offset of local header
494
+ header.set(nameBytes, 46);
495
+ return header;
496
+ }
497
+ createEndOfCentralDirectory(numFiles, centralDirSize, centralDirOffset) {
498
+ const header = new Uint8Array(22);
499
+ const view = new DataView(header.buffer);
500
+ view.setUint32(0, 0x06054b50, true); // End of central directory signature
501
+ view.setUint16(4, 0, true); // Number of this disk
502
+ view.setUint16(6, 0, true); // Disk where central directory starts
503
+ view.setUint16(8, numFiles, true); // Number of central directory records on this disk
504
+ view.setUint16(10, numFiles, true); // Total number of central directory records
505
+ view.setUint32(12, centralDirSize, true); // Size of central directory
506
+ view.setUint32(16, centralDirOffset, true); // Offset of start of central directory
507
+ view.setUint16(20, 0, true); // Comment length
508
+ return header;
509
+ }
510
+ crc32(data) {
511
+ let crc = 0xFFFFFFFF;
512
+ for (let i = 0; i < data.length; i++) {
513
+ crc ^= data[i];
514
+ for (let j = 0; j < 8; j++) {
515
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
516
+ }
517
+ }
518
+ return (crc ^ 0xFFFFFFFF) >>> 0;
519
+ }
520
+ }
521
+ class ExcelWriter {
522
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
523
+ static async write(workbook, _options) {
524
+ const zip = new ZipWriter();
525
+ const sheets = workbook['sheets'];
526
+ const sheetNames = sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`);
527
+ // Collect all styles from sheets
528
+ const styleRegistry = this.collectStyles(sheets);
529
+ // Add required XLSX files
530
+ zip.addFile('[Content_Types].xml', this.generateContentTypes(sheetNames));
531
+ zip.addFile('_rels/.rels', this.generateRels());
532
+ zip.addFile('xl/workbook.xml', this.generateWorkbook(sheetNames));
533
+ zip.addFile('xl/_rels/workbook.xml.rels', this.generateWorkbookRels(sheetNames));
534
+ zip.addFile('xl/styles.xml', this.generateStyles(styleRegistry));
535
+ zip.addFile('xl/sharedStrings.xml', this.generateSharedStrings(sheets));
536
+ // Add each worksheet
537
+ sheets.forEach((sheet, index) => {
538
+ zip.addFile(`xl/worksheets/sheet${index + 1}.xml`, this.generateWorksheet(sheet, styleRegistry));
539
+ });
540
+ const buffer = zip.generate();
541
+ return new Blob([buffer.buffer], {
542
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
543
+ });
544
+ }
545
+ /**
546
+ * Collect all unique styles from sheets
547
+ */
548
+ static collectStyles(sheets) {
549
+ const registry = {
550
+ fonts: new Map(),
551
+ fills: new Map(),
552
+ borders: new Map(),
553
+ cellXfs: new Map()
554
+ };
555
+ // Add default font (Calibri 11)
556
+ const defaultFont = this.serializeFont({});
557
+ registry.fonts.set(defaultFont, 0);
558
+ // Add required fills (none and gray125)
559
+ registry.fills.set('none', 0);
560
+ registry.fills.set('gray125', 1);
561
+ // Add default border
562
+ registry.borders.set('none', 0);
563
+ // Add default cellXf (no style)
564
+ registry.cellXfs.set('default', 0);
565
+ // Collect styles from all cells
566
+ sheets.forEach((sheet) => {
567
+ const rows = sheet.getRows();
568
+ rows.forEach((row) => {
569
+ row.getCells().forEach((cell) => {
570
+ const cellData = cell.toData();
571
+ if (cellData.style) {
572
+ this.registerStyle(cellData.style, registry);
573
+ }
574
+ });
575
+ });
576
+ });
577
+ return registry;
578
+ }
579
+ /**
580
+ * Register a style and get its cellXf index
581
+ */
582
+ static registerStyle(style, registry) {
583
+ var _a, _b, _c, _d;
584
+ // Serialize and register font
585
+ const fontKey = this.serializeFont(style);
586
+ if (!registry.fonts.has(fontKey)) {
587
+ registry.fonts.set(fontKey, registry.fonts.size);
588
+ }
589
+ // Serialize and register fill
590
+ const fillKey = this.serializeFill(style);
591
+ if (fillKey !== 'none' && !registry.fills.has(fillKey)) {
592
+ registry.fills.set(fillKey, registry.fills.size);
593
+ }
594
+ // Serialize and register border
595
+ const borderKey = this.serializeBorder(style);
596
+ if (borderKey !== 'none' && !registry.borders.has(borderKey)) {
597
+ registry.borders.set(borderKey, registry.borders.size);
598
+ }
599
+ // Create cellXf key
600
+ const fontIndex = (_a = registry.fonts.get(fontKey)) !== null && _a !== void 0 ? _a : 0;
601
+ const fillIndex = (_b = registry.fills.get(fillKey)) !== null && _b !== void 0 ? _b : 0;
602
+ const borderIndex = (_c = registry.borders.get(borderKey)) !== null && _c !== void 0 ? _c : 0;
603
+ let alignment = this.serializeAlignment(style);
604
+ if (!alignment)
605
+ alignment = '{}';
606
+ const xfKey = `f${fontIndex}:l${fillIndex}:b${borderIndex}:a${alignment}`;
607
+ if (!registry.cellXfs.has(xfKey)) {
608
+ registry.cellXfs.set(xfKey, registry.cellXfs.size);
609
+ }
610
+ return (_d = registry.cellXfs.get(xfKey)) !== null && _d !== void 0 ? _d : 0;
611
+ }
612
+ /**
613
+ * Get style index for a cell style
614
+ */
615
+ static getStyleIndex(style, registry) {
616
+ if (!style)
617
+ return 0;
618
+ return this.registerStyle(style, registry);
619
+ }
620
+ static serializeFont(style) {
621
+ var _a, _b, _c, _d, _e, _f;
622
+ const bold = style.bold || ((_a = style.font) === null || _a === void 0 ? void 0 : _a.bold);
623
+ const italic = style.italic || ((_b = style.font) === null || _b === void 0 ? void 0 : _b.italic);
624
+ const underline = style.underline || ((_c = style.font) === null || _c === void 0 ? void 0 : _c.underline);
625
+ const color = style.color || ((_d = style.font) === null || _d === void 0 ? void 0 : _d.color);
626
+ const size = style.fontSize || ((_e = style.font) === null || _e === void 0 ? void 0 : _e.size) || 11;
627
+ const name = style.fontFamily || ((_f = style.font) === null || _f === void 0 ? void 0 : _f.name) || 'Calibri';
628
+ return JSON.stringify({ bold, italic, underline, color, size, name });
629
+ }
630
+ static serializeFill(style) {
631
+ var _a;
632
+ const bgColor = style.backgroundColor || ((_a = style.fill) === null || _a === void 0 ? void 0 : _a.fgColor);
633
+ if (!bgColor)
634
+ return 'none';
635
+ return JSON.stringify({ bgColor });
636
+ }
637
+ static serializeBorder(style) {
638
+ if (!style.border && !style.borderAll)
639
+ return 'none';
640
+ return JSON.stringify({ border: style.border, borderAll: style.borderAll });
641
+ }
642
+ static serializeAlignment(style) {
643
+ const h = style.alignment;
644
+ const v = style.verticalAlignment;
645
+ const wrap = style.wrapText;
646
+ if (!h && !v && !wrap)
647
+ return '';
648
+ return JSON.stringify({ h, v, wrap });
649
+ }
650
+ static escapeXml(str) {
651
+ if (typeof str !== 'string')
652
+ return String(str !== null && str !== void 0 ? str : '');
653
+ return str
654
+ .replace(/&/g, '&amp;')
655
+ .replace(/</g, '&lt;')
656
+ .replace(/>/g, '&gt;')
657
+ .replace(/"/g, '&quot;')
658
+ .replace(/'/g, '&apos;');
659
+ }
660
+ static generateContentTypes(sheetNames) {
661
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
662
+ xml += '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
663
+ xml += '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
664
+ xml += '<Default Extension="xml" ContentType="application/xml"/>';
665
+ xml += '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
666
+ xml += '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
667
+ xml += '<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>';
668
+ sheetNames.forEach((_, index) => {
669
+ xml += `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
670
+ });
671
+ xml += '</Types>';
672
+ return xml;
673
+ }
674
+ static generateRels() {
675
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
676
+ xml += '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
677
+ xml += '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>';
678
+ xml += '</Relationships>';
679
+ return xml;
680
+ }
681
+ static generateWorkbook(sheetNames) {
682
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
683
+ xml += '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
684
+ xml += '<sheets>';
685
+ sheetNames.forEach((name, index) => {
686
+ xml += `<sheet name="${this.escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`;
687
+ });
688
+ xml += '</sheets>';
689
+ xml += '</workbook>';
690
+ return xml;
691
+ }
692
+ static generateWorkbookRels(sheetNames) {
693
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
694
+ xml += '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
695
+ sheetNames.forEach((_, index) => {
696
+ xml += `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`;
697
+ });
698
+ xml += `<Relationship Id="rId${sheetNames.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
699
+ xml += `<Relationship Id="rId${sheetNames.length + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`;
700
+ xml += '</Relationships>';
701
+ return xml;
702
+ }
703
+ static generateStyles(registry) {
704
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
705
+ xml += '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
706
+ // Generate fonts
707
+ xml += `<fonts count="${registry.fonts.size}">`;
708
+ const fontEntries = Array.from(registry.fonts.entries()).sort((a, b) => a[1] - b[1]);
709
+ for (const [fontKey] of fontEntries) {
710
+ const font = JSON.parse(fontKey);
711
+ xml += '<font>';
712
+ if (font.bold)
713
+ xml += '<b/>';
714
+ if (font.italic)
715
+ xml += '<i/>';
716
+ if (font.underline)
717
+ xml += '<u/>';
718
+ xml += `<sz val="${font.size || 11}"/>`;
719
+ if (font.color) {
720
+ const colorHex = this.colorToARGB(font.color);
721
+ xml += `<color rgb="${colorHex}"/>`;
722
+ }
723
+ xml += `<name val="${font.name || 'Calibri'}"/>`;
724
+ xml += '</font>';
725
+ }
726
+ xml += '</fonts>';
727
+ // Generate fills
728
+ xml += `<fills count="${registry.fills.size}">`;
729
+ const fillEntries = Array.from(registry.fills.entries()).sort((a, b) => a[1] - b[1]);
730
+ for (const [fillKey] of fillEntries) {
731
+ if (fillKey === 'none') {
732
+ xml += '<fill><patternFill patternType="none"/></fill>';
733
+ }
734
+ else if (fillKey === 'gray125') {
735
+ xml += '<fill><patternFill patternType="gray125"/></fill>';
736
+ }
737
+ else {
738
+ const fill = JSON.parse(fillKey);
739
+ const colorHex = this.colorToARGB(fill.bgColor);
740
+ xml += `<fill><patternFill patternType="solid"><fgColor rgb="${colorHex}"/><bgColor indexed="64"/></patternFill></fill>`;
741
+ }
742
+ }
743
+ xml += '</fills>';
744
+ // Generate borders
745
+ xml += `<borders count="${registry.borders.size}">`;
746
+ const borderEntries = Array.from(registry.borders.entries()).sort((a, b) => a[1] - b[1]);
747
+ for (const [borderKey] of borderEntries) {
748
+ if (borderKey === 'none') {
749
+ xml += '<border><left/><right/><top/><bottom/><diagonal/></border>';
750
+ }
751
+ else {
752
+ const borderData = JSON.parse(borderKey);
753
+ xml += '<border>';
754
+ if (borderData.borderAll) {
755
+ const style = borderData.borderAll.style || 'thin';
756
+ const color = borderData.borderAll.color ? this.colorToARGB(borderData.borderAll.color) : 'FF000000';
757
+ xml += `<left style="${style}"><color rgb="${color}"/></left>`;
758
+ xml += `<right style="${style}"><color rgb="${color}"/></right>`;
759
+ xml += `<top style="${style}"><color rgb="${color}"/></top>`;
760
+ xml += `<bottom style="${style}"><color rgb="${color}"/></bottom>`;
761
+ }
762
+ else if (borderData.border) {
763
+ const b = borderData.border;
764
+ xml += this.generateBorderEdge('left', b.left);
765
+ xml += this.generateBorderEdge('right', b.right);
766
+ xml += this.generateBorderEdge('top', b.top);
767
+ xml += this.generateBorderEdge('bottom', b.bottom);
768
+ }
769
+ xml += '<diagonal/></border>';
770
+ }
771
+ }
772
+ xml += '</borders>';
773
+ // Generate cellStyleXfs (base styles)
774
+ xml += '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>';
775
+ // Generate cellXfs (cell formats)
776
+ xml += `<cellXfs count="${registry.cellXfs.size}">`;
777
+ const xfEntries = Array.from(registry.cellXfs.entries()).sort((a, b) => a[1] - b[1]);
778
+ for (const [xfKey] of xfEntries) {
779
+ if (xfKey === 'default') {
780
+ xml += '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>';
781
+ }
782
+ else {
783
+ // Parse xf key: f{fontIndex}:l{fillIndex}:b{borderIndex}:a{alignment}
784
+ const parts = xfKey.split(':');
785
+ const fontId = parseInt(parts[0].substring(1)) || 0;
786
+ const fillId = parseInt(parts[1].substring(1)) || 0;
787
+ const borderId = parseInt(parts[2].substring(1)) || 0;
788
+ const alignmentJson = parts[3].substring(1);
789
+ let xf = `<xf numFmtId="0" fontId="${fontId}" fillId="${fillId}" borderId="${borderId}" xfId="0"`;
790
+ if (fontId > 0)
791
+ xf += ' applyFont="1"';
792
+ if (fillId > 0)
793
+ xf += ' applyFill="1"';
794
+ if (borderId > 0)
795
+ xf += ' applyBorder="1"';
796
+ let align = {};
797
+ if (alignmentJson && alignmentJson.trim().startsWith('{')) {
798
+ try {
799
+ align = JSON.parse(alignmentJson);
800
+ }
801
+ catch (e) {
802
+ align = {};
803
+ }
804
+ }
805
+ if (Object.keys(align).length > 0) {
806
+ xf += ' applyAlignment="1">';
807
+ xf += '<alignment';
808
+ if (align.h)
809
+ xf += ` horizontal="${align.h}"`;
810
+ if (align.v)
811
+ xf += ` vertical="${align.v === 'middle' ? 'center' : align.v}"`;
812
+ if (align.wrap)
813
+ xf += ' wrapText="1"';
814
+ xf += '/></xf>';
815
+ }
816
+ else {
817
+ xf += '/>';
818
+ }
819
+ xml += xf;
820
+ }
821
+ }
822
+ xml += '</cellXfs>';
823
+ xml += '</styleSheet>';
824
+ return xml;
825
+ }
826
+ static generateBorderEdge(side, edge) {
827
+ if (!edge || !edge.style)
828
+ return `<${side}/>`;
829
+ const color = edge.color ? this.colorToARGB(edge.color) : 'FF000000';
830
+ return `<${side} style="${edge.style}"><color rgb="${color}"/></${side}>`;
831
+ }
832
+ static colorToARGB(color) {
833
+ if (!color)
834
+ return 'FF000000';
835
+ // Remove # if present
836
+ let hex = color.replace('#', '').toUpperCase();
837
+ // Add alpha if not present (6 chars -> 8 chars)
838
+ if (hex.length === 6) {
839
+ hex = 'FF' + hex;
840
+ }
841
+ // Handle 3 char hex
842
+ if (hex.length === 3) {
843
+ hex = 'FF' + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
844
+ }
845
+ return hex;
846
+ }
847
+ static generateSharedStrings(sheets) {
848
+ const strings = [];
849
+ sheets.forEach((sheet) => {
850
+ const rows = sheet.getRows();
851
+ rows.forEach((row) => {
852
+ row.getCells().forEach((cell) => {
853
+ const cellData = cell.toData();
854
+ if (typeof cellData.value === 'string' && !cellData.formula) {
855
+ if (!strings.includes(cellData.value)) {
856
+ strings.push(cellData.value);
857
+ }
858
+ }
859
+ });
860
+ });
861
+ });
862
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
863
+ xml += `<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${strings.length}" uniqueCount="${strings.length}">`;
864
+ strings.forEach(str => {
865
+ xml += `<si><t>${this.escapeXml(str)}</t></si>`;
866
+ });
867
+ xml += '</sst>';
868
+ return xml;
869
+ }
870
+ static generateWorksheet(sheet, styleRegistry) {
871
+ const rows = sheet.getRows();
872
+ const columns = sheet.getColumns();
873
+ const allStrings = this.collectAllStrings(sheet);
874
+ let xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
875
+ xml += '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
876
+ // Add sheet dimension
877
+ const dimension = this.getSheetDimension(rows);
878
+ xml += `<dimension ref="${dimension}"/>`;
879
+ // Add column widths if defined
880
+ if (columns.length > 0) {
881
+ xml += '<cols>';
882
+ columns.forEach((col, index) => {
883
+ const colData = col.toData();
884
+ if (colData.width !== undefined && colData.width > 0) {
885
+ // Excel width is in character units, convert from pixels: width / 7
886
+ const excelWidth = colData.width / 7;
887
+ xml += `<col min="${index + 1}" max="${index + 1}" width="${excelWidth}" customWidth="1"`;
888
+ if (colData.hidden) {
889
+ xml += ' hidden="1"';
890
+ }
891
+ xml += '/>';
892
+ }
893
+ });
894
+ xml += '</cols>';
895
+ }
896
+ xml += '<sheetData>';
897
+ rows.forEach((row, rowIndex) => {
898
+ const cells = row.getCells();
899
+ if (cells.length > 0) {
900
+ xml += `<row r="${rowIndex + 1}">`;
901
+ cells.forEach((cell, colIndex) => {
902
+ const cellData = cell.toData();
903
+ const cellRef = this.columnToLetter(colIndex + 1) + (rowIndex + 1);
904
+ const styleIndex = this.getStyleIndex(cellData.style, styleRegistry);
905
+ const styleAttr = styleIndex > 0 ? ` s="${styleIndex}"` : '';
906
+ if (cellData.formula) {
907
+ xml += `<c r="${cellRef}"${styleAttr}><f>${this.escapeXml(cellData.formula)}</f></c>`;
908
+ }
909
+ else if (cellData.value !== null && cellData.value !== undefined && cellData.value !== '') {
910
+ if (typeof cellData.value === 'number') {
911
+ xml += `<c r="${cellRef}"${styleAttr}><v>${cellData.value}</v></c>`;
912
+ }
913
+ else if (typeof cellData.value === 'boolean') {
914
+ xml += `<c r="${cellRef}"${styleAttr} t="b"><v>${cellData.value ? 1 : 0}</v></c>`;
915
+ }
916
+ else if (cellData.type === 'date') {
917
+ const excelDate = DateFormatter.toExcelDate(cellData.value);
918
+ xml += `<c r="${cellRef}"${styleAttr}><v>${excelDate}</v></c>`;
919
+ }
920
+ else {
921
+ // String value - use shared string index
922
+ const stringIndex = allStrings.indexOf(String(cellData.value));
923
+ if (stringIndex >= 0) {
924
+ xml += `<c r="${cellRef}"${styleAttr} t="s"><v>${stringIndex}</v></c>`;
925
+ }
926
+ else {
927
+ // Inline string as fallback
928
+ xml += `<c r="${cellRef}"${styleAttr} t="inlineStr"><is><t>${this.escapeXml(String(cellData.value))}</t></is></c>`;
929
+ }
930
+ }
931
+ }
932
+ else if (styleIndex > 0) {
933
+ // Empty cell with style
934
+ xml += `<c r="${cellRef}"${styleAttr}/>`;
935
+ }
936
+ });
937
+ xml += '</row>';
938
+ }
939
+ });
940
+ xml += '</sheetData>';
941
+ // Add merged cells if any
942
+ const sheetData = sheet.toData();
943
+ if (sheetData.mergeCells && sheetData.mergeCells.length > 0) {
944
+ xml += '<mergeCells>';
945
+ sheetData.mergeCells.forEach(merge => {
946
+ const startRef = this.columnToLetter(merge.startCol + 1) + (merge.startRow + 1);
947
+ const endRef = this.columnToLetter(merge.endCol + 1) + (merge.endRow + 1);
948
+ xml += `<mergeCell ref="${startRef}:${endRef}"/>`;
949
+ });
950
+ xml += '</mergeCells>';
951
+ }
952
+ xml += '</worksheet>';
953
+ return xml;
954
+ }
955
+ static collectAllStrings(sheet) {
956
+ const strings = [];
957
+ const rows = sheet.getRows();
958
+ rows.forEach((row) => {
959
+ row.getCells().forEach((cell) => {
960
+ const cellData = cell.toData();
961
+ if (typeof cellData.value === 'string' && !cellData.formula) {
962
+ if (!strings.includes(cellData.value)) {
963
+ strings.push(cellData.value);
964
+ }
965
+ }
966
+ });
967
+ });
968
+ return strings;
969
+ }
970
+ static getSheetDimension(rows) {
971
+ if (rows.length === 0)
972
+ return 'A1';
973
+ let maxCol = 1;
974
+ rows.forEach(row => {
975
+ const cellCount = row.getCells().length;
976
+ if (cellCount > maxCol)
977
+ maxCol = cellCount;
978
+ });
979
+ return `A1:${this.columnToLetter(maxCol)}${rows.length}`;
980
+ }
981
+ static columnToLetter(column) {
982
+ let letters = '';
983
+ while (column > 0) {
984
+ const temp = (column - 1) % 26;
985
+ letters = String.fromCharCode(temp + 65) + letters;
986
+ column = Math.floor((column - temp - 1) / 26);
987
+ }
988
+ return letters || 'A';
989
+ }
990
+ }
991
+
405
992
  class CSVWriter {
406
993
  static async write(workbook, _options) {
407
994
  const sheets = workbook['sheets'];
@@ -473,102 +1060,6 @@ class JSONWriter {
473
1060
  }
474
1061
  }
475
1062
 
476
- class ExcelWriter {
477
- static async write(workbook, _options) {
478
- const data = this.generateExcelData(workbook);
479
- const buffer = this.createExcelFile(data);
480
- const uint8Buffer = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
481
- const arrayBuffer = uint8Buffer.slice().buffer;
482
- return new Blob([arrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
483
- }
484
- static generateExcelData(workbook) {
485
- const sheets = workbook['sheets'];
486
- return {
487
- Sheets: sheets.reduce((acc, sheet, index) => {
488
- const sheetName = sheet.getName() || `Sheet${index + 1}`;
489
- acc[sheetName] = this.generateSheetData(sheet);
490
- return acc;
491
- }, {}),
492
- SheetNames: sheets.map((sheet, index) => sheet.getName() || `Sheet${index + 1}`)
493
- };
494
- }
495
- static generateSheetData(sheet) {
496
- const rows = sheet.getRows();
497
- const data = [];
498
- rows.forEach((row) => {
499
- const rowData = [];
500
- row.getCells().forEach((cell) => {
501
- const cellData = cell.toData();
502
- if (cellData.formula) {
503
- rowData.push({ f: cellData.formula });
504
- }
505
- else if (cellData.type === 'date' &&
506
- (typeof cellData.value === 'string' ||
507
- typeof cellData.value === 'number' ||
508
- cellData.value instanceof Date)) {
509
- rowData.push(DateFormatter.toExcelDate(cellData.value));
510
- }
511
- else {
512
- rowData.push(cellData.value);
513
- }
514
- });
515
- data.push(rowData);
516
- });
517
- return {
518
- '!ref': this.getSheetRange(data),
519
- '!rows': rows.map(row => ({
520
- hpt: row['height'],
521
- hidden: row['hidden'],
522
- outlineLevel: row['outlineLevel'],
523
- collapsed: row['collapsed']
524
- })),
525
- '!cols': sheet.getColumns().map(col => ({
526
- wch: col['width'],
527
- hidden: col['hidden'],
528
- outlineLevel: col['outlineLevel'],
529
- collapsed: col['collapsed']
530
- })),
531
- '!merges': sheet['mergeCells'],
532
- '!freeze': sheet['freezePane']
533
- };
534
- }
535
- static getSheetRange(data) {
536
- if (data.length === 0)
537
- return 'A1:A1';
538
- const lastRow = data.length;
539
- const lastCol = Math.max(...data.map(row => row.length));
540
- return `A1:${this.columnToLetter(lastCol)}${lastRow}`;
541
- }
542
- static columnToLetter(column) {
543
- let letters = '';
544
- while (column > 0) {
545
- const temp = (column - 1) % 26;
546
- letters = String.fromCharCode(temp + 65) + letters;
547
- column = (column - temp - 1) / 26;
548
- }
549
- return letters || 'A';
550
- }
551
- static createExcelFile(data) {
552
- // This is a simplified Excel file generator
553
- // In a real implementation, you'd generate proper XLSX binary format
554
- // For now, we'll return a simple XML-based structure
555
- const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
556
- const workbook = this.generateWorkbookXML(data);
557
- const encoder = new TextEncoder();
558
- return encoder.encode(xmlHeader + workbook);
559
- }
560
- static generateWorkbookXML(data) {
561
- let xml = '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">';
562
- xml += '<sheets>';
563
- data.SheetNames.forEach((name, index) => {
564
- xml += `<sheet name="${name}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`;
565
- });
566
- xml += '</sheets>';
567
- xml += '</workbook>';
568
- return xml;
569
- }
570
- }
571
-
572
1063
  class Workbook {
573
1064
  constructor(data) {
574
1065
  this.sheets = [];
@@ -826,6 +1317,55 @@ class ExportBuilder {
826
1317
  });
827
1318
  return this;
828
1319
  }
1320
+ /**
1321
+ * Merge cells in the current worksheet
1322
+ * @param startRow Start row index (0-based)
1323
+ * @param startCol Start column index (0-based)
1324
+ * @param endRow End row index (0-based)
1325
+ * @param endCol End column index (0-based)
1326
+ */
1327
+ mergeCells(startRow, startCol, endRow, endCol) {
1328
+ this.currentSheet.mergeCells(startRow, startCol, endRow, endCol);
1329
+ return this;
1330
+ }
1331
+ /**
1332
+ * Set alignment for a specific cell
1333
+ * @param row Row index (0-based)
1334
+ * @param col Column index (0-based)
1335
+ * @param horizontal Horizontal alignment
1336
+ * @param vertical Vertical alignment (optional)
1337
+ */
1338
+ setAlignment(row, col, horizontal, vertical) {
1339
+ const rowObj = this.currentSheet.getRow(row);
1340
+ if (rowObj) {
1341
+ const cells = rowObj.getCells();
1342
+ if (cells[col]) {
1343
+ const style = StyleBuilder.create().align(horizontal);
1344
+ if (vertical) {
1345
+ style.verticalAlign(vertical);
1346
+ }
1347
+ cells[col].setStyle(style.build());
1348
+ }
1349
+ }
1350
+ return this;
1351
+ }
1352
+ /**
1353
+ * Set alignment for a range of cells
1354
+ * @param startRow Start row index (0-based)
1355
+ * @param startCol Start column index (0-based)
1356
+ * @param endRow End row index (0-based)
1357
+ * @param endCol End column index (0-based)
1358
+ * @param horizontal Horizontal alignment
1359
+ * @param vertical Vertical alignment (optional)
1360
+ */
1361
+ setRangeAlignment(startRow, startCol, endRow, endCol, horizontal, vertical) {
1362
+ for (let r = startRow; r <= endRow; r++) {
1363
+ for (let c = startCol; c <= endCol; c++) {
1364
+ this.setAlignment(r, c, horizontal, vertical);
1365
+ }
1366
+ }
1367
+ return this;
1368
+ }
829
1369
  autoSizeColumns() {
830
1370
  const rows = this.currentSheet.getRows();
831
1371
  const maxLengths = [];
@@ -950,6 +1490,28 @@ class SheetBuilder {
950
1490
  });
951
1491
  return this;
952
1492
  }
1493
+ /**
1494
+ * Set the width of a specific column
1495
+ * @param colIndex Column index
1496
+ * @param width Width to set
1497
+ */
1498
+ setColumnWidth(colIndex, width) {
1499
+ const column = this.getOrCreateColumn(colIndex);
1500
+ column.setWidth(width);
1501
+ return this;
1502
+ }
1503
+ /**
1504
+ * Set the height of a specific row
1505
+ * @param rowIndex Row index
1506
+ * @param height Height to set
1507
+ */
1508
+ setRowHeight(rowIndex, height) {
1509
+ const row = this.worksheet.getRow(rowIndex);
1510
+ if (row) {
1511
+ row.setHeight(height);
1512
+ }
1513
+ return this;
1514
+ }
953
1515
  /**
954
1516
  * Add a title row
955
1517
  * @param title Title text