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),
});
}