export type XlsxCellValue = string | number | boolean | null | undefined; export interface XlsxWorksheetData { sheetName?: string; columns: readonly string[]; rows: readonly (readonly XlsxCellValue[])[]; } type ZipEntry = { path: string; data: Uint8Array; crc: number; localHeaderOffset: number; }; const encoder = new TextEncoder(); const CRC_TABLE = buildCrcTable(); function buildCrcTable(): number[] { const table: number[] = []; for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } table.push(c >>> 0); } return table; } function crc32(data: Uint8Array): number { let crc = 0xffffffff; for (const byte of data) { crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } function escapeXml(value: string): string { return [...value] .filter((char) => { const code = char.charCodeAt(0); return code === 9 || code === 10 || code === 13 || code >= 32; }) .join("") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function columnName(index: number): string { let value = ""; let n = index + 1; while (n > 0) { const rem = (n - 1) % 26; value = String.fromCharCode(65 + rem) + value; n = Math.floor((n - 1) / 26); } return value; } function cellRef(rowIndex: number, colIndex: number): string { return `${columnName(colIndex)}${rowIndex + 1}`; } function sheetRange(columnCount: number, rowCount: number): string { if (columnCount === 0 || rowCount === 0) return "A1"; return `A1:${columnName(columnCount - 1)}${rowCount}`; } function normalizeSheetName(value?: string): string { const invalidChars = new Set(["[", "]", ":", "*", "?", "/", "\\"]); const name = [...(value || "Sheet1")] .map((char) => (invalidChars.has(char) ? " " : char)) .join("") .trim() || "Sheet1"; return name.slice(0, 31); } function estimateColumnWidths(columns: readonly string[], rows: readonly (readonly XlsxCellValue[])[]): number[] { return columns.map((column, colIndex) => { const values = rows.slice(0, 100).map((row) => row[colIndex]); const maxLen = [column, ...values.map((value) => (value == null ? "" : String(value)))] .map((value) => Math.min(value.length, 60)) .reduce((max, length) => Math.max(max, length), 8); return Math.max(10, Math.min(60, maxLen + 2)); }); } function cellXml(value: XlsxCellValue, rowIndex: number, colIndex: number, style?: number): string { const ref = cellRef(rowIndex, colIndex); const styleAttr = style == null ? "" : ` s="${style}"`; if (value == null) return ``; if (typeof value === "number" && Number.isFinite(value)) { return `${value}`; } if (typeof value === "boolean") { return `${value ? 1 : 0}`; } return `${escapeXml(String(value))}`; } function worksheetXml(data: XlsxWorksheetData): string { const columns = data.columns; const rows = data.rows; const totalRows = rows.length + 1; const range = sheetRange(columns.length, totalRows); const widths = estimateColumnWidths(columns, rows); const colsXml = widths .map((width, index) => ``) .join(""); const headerXml = `${columns.map((column, index) => cellXml(column, 0, index, 1)).join("")}`; const bodyXml = rows .map((row, rowIndex) => { const excelRowIndex = rowIndex + 2; const cells = columns.map((_, colIndex) => cellXml(row[colIndex], excelRowIndex - 1, colIndex)).join(""); return `${cells}`; }) .join(""); return ` ${colsXml} ${headerXml}${bodyXml} `; } function contentTypesXml(): string { return ` `; } function rootRelsXml(): string { return ` `; } function workbookXml(sheetName: string): string { return ` `; } function workbookRelsXml(): string { return ` `; } function stylesXml(): string { return ` `; } function uint16(value: number): Uint8Array { const bytes = new Uint8Array(2); const view = new DataView(bytes.buffer); view.setUint16(0, value, true); return bytes; } function uint32(value: number): Uint8Array { const bytes = new Uint8Array(4); const view = new DataView(bytes.buffer); view.setUint32(0, value >>> 0, true); return bytes; } function concatBytes(parts: Uint8Array[]): Uint8Array { const total = parts.reduce((sum, part) => sum + part.length, 0); const out = new Uint8Array(total); let offset = 0; for (const part of parts) { out.set(part, offset); offset += part.length; } return out; } function createZip(files: Array<{ path: string; content: string }>): Uint8Array { const entries: ZipEntry[] = []; const localParts: Uint8Array[] = []; let offset = 0; for (const file of files) { const path = encoder.encode(file.path); const data = encoder.encode(file.content); const crc = crc32(data); const localHeader = concatBytes([ uint32(0x04034b50), uint16(20), uint16(0), uint16(0), uint16(0), uint16(0), uint32(crc), uint32(data.length), uint32(data.length), uint16(path.length), uint16(0), path, ]); entries.push({ path: file.path, data, crc, localHeaderOffset: offset }); localParts.push(localHeader, data); offset += localHeader.length + data.length; } const centralParts: Uint8Array[] = []; for (const entry of entries) { const path = encoder.encode(entry.path); centralParts.push( concatBytes([ uint32(0x02014b50), uint16(20), uint16(20), uint16(0), uint16(0), uint16(0), uint16(0), uint32(entry.crc), uint32(entry.data.length), uint32(entry.data.length), uint16(path.length), uint16(0), uint16(0), uint16(0), uint16(0), uint32(0), uint32(entry.localHeaderOffset), path, ]), ); } const central = concatBytes(centralParts); const end = concatBytes([ uint32(0x06054b50), uint16(0), uint16(0), uint16(entries.length), uint16(entries.length), uint32(central.length), uint32(offset), uint16(0), ]); return concatBytes([...localParts, central, end]); } export function buildXlsxWorkbook(data: XlsxWorksheetData): Uint8Array { const sheetName = normalizeSheetName(data.sheetName); return createZip([ { path: "[Content_Types].xml", content: contentTypesXml() }, { path: "_rels/.rels", content: rootRelsXml() }, { path: "xl/workbook.xml", content: workbookXml(sheetName) }, { path: "xl/_rels/workbook.xml.rels", content: workbookRelsXml() }, { path: "xl/styles.xml", content: stylesXml() }, { path: "xl/worksheets/sheet1.xml", content: worksheetXml(data) }, ]); }