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/README.md +151 -10
- package/dist/index.esm.js +658 -96
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +658 -96
- package/dist/index.js.map +1 -1
- package/dist/types/builders/export-builder.d.ts +26 -0
- package/dist/types/builders/sheet-builder.d.ts +12 -0
- package/dist/types/index.d.ts +66 -5
- package/dist/types/writers/excel-writer.d.ts +28 -10
- package/package.json +2 -2
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, '&')
|
|
657
|
+
.replace(/</g, '<')
|
|
658
|
+
.replace(/>/g, '>')
|
|
659
|
+
.replace(/"/g, '"')
|
|
660
|
+
.replace(/'/g, ''');
|
|
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
|