/** * Shared Excel → HTML table renderer. * * Converts a worksheet to a styled HTML table with: * - Column letter headers (A, B, C…) * - Row numbers * - Proper empty cell handling * - Sanitized output */ import type { WorkSheet } from 'xlsx'; /** Convert column index (0-based) to Excel-style letter (A, B, …, Z, AA, AB, …) */ const colLetter = (i: number): string => { let s = ''; let n = i; while (n >= 0) { s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26) - 1; } return s; }; /** Escape HTML entities */ const esc = (v: unknown): string => { if (v === null || v === undefined || v === '') return ' '; return String(v).replace(/&/g, '&').replace(//g, '>'); }; export interface ExcelTableResult { html: string; rowCount: number; colCount: number; } /** * Render a worksheet as an HTML table string. * Uses sheet_to_json with header:1 for a raw 2D array. */ export async function excelToTable(worksheet: WorkSheet): Promise { const XLSX = await import('xlsx'); const rows: unknown[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' }); if (rows.length === 0) { return { html: '
 
', rowCount: 0, colCount: 0 }; } // Determine column count from the widest row const colCount = rows.reduce((max, row) => Math.max(max, row.length), 0); const rowCount = rows.length; const parts: string[] = []; parts.push(''); // Column letter header row parts.push(''); parts.push(''); // corner cell for (let c = 0; c < colCount; c++) { parts.push(``); } parts.push(''); // Data rows parts.push(''); for (let r = 0; r < rowCount; r++) { const row = rows[r]; parts.push(''); parts.push(``); for (let c = 0; c < colCount; c++) { const val = c < row.length ? row[c] : ''; const isNum = typeof val === 'number'; parts.push(`${esc(val)}`); } parts.push(''); } parts.push('
${colLetter(c)}
${r + 1}
'); const DOMPurify = (await import('dompurify')).default; return { html: DOMPurify.sanitize(parts.join('')), rowCount, colCount }; }