import { zipSync, strToU8 } from "fflate"; import type { ColumnInfo } from "./types"; /** Minimal OOXML spreadsheet: one sheet, every cell as an inline string * (t="inlineStr") so Excel never evaluates a cell as a formula — the * formula-injection mitigation required by the spec. */ const XML_ESCAPES: [RegExp, string][] = [ [/&/g, "&"], [//g, ">"], [/"/g, """], ]; function esc(s: string): string { for (const [re, w] of XML_ESCAPES) s = s.replace(re, w); return s; } function colLetter(n: number): string { let s = ""; for (let i = n; i > 0; i = Math.floor((i - 1) / 26)) s = String.fromCharCode(65 + ((i - 1) % 26)) + s; return s; } function buildColsXML(columns: ColumnInfo[]): string { const maxW = columns.map((c) => Math.min(60, Math.max(8, c.name.length + 2))); return `${maxW.map((w, i) => `\n `).join("")}\n`; } function buildCellsXML(rows: unknown[][], columns: ColumnInfo[]): string { let cells = ""; cells += `` + columns .map((c, i) => `${esc(c.name)}`) .join("") + ``; rows.forEach((row, rIdx) => { const r = rIdx + 2; cells += `` + row .map((v, i) => { const ref = `${colLetter(i + 1)}${r}`; const t = v === null || v === undefined ? "" : String(v); return `${esc(t)}`; }) .join("") + ``; }); return cells; } export function buildXlsx(rows: unknown[][], columns: ColumnInfo[]): Uint8Array { const colsXML = buildColsXML(columns); const cells = buildCellsXML(rows, columns); const sheet1 = `\n${colsXML}${cells}`; const workbook = `\n`; const ct = `\n`; const rels = `\n`; const rootRels = `\n`; return zipSync({ "[Content_Types].xml": strToU8(ct), "_rels/.rels": strToU8(rootRels), "xl/workbook.xml": strToU8(workbook), "xl/_rels/workbook.xml.rels": strToU8(rels), "xl/worksheets/sheet1.xml": strToU8(sheet1), }); }