File size: 2,576 Bytes
bf4f454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 * 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
};

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<ExcelTableResult> {
    const XLSX = await import('xlsx');
    const rows: unknown[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });

    if (rows.length === 0) {
        return { html: '<table><tbody><tr><td>&nbsp;</td></tr></tbody></table>', 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('<table>');

    // Column letter header row
    parts.push('<thead><tr>');
    parts.push('<th class="excel-row-num"></th>'); // corner cell
    for (let c = 0; c < colCount; c++) {
        parts.push(`<th class="excel-col-hdr">${colLetter(c)}</th>`);
    }
    parts.push('</tr></thead>');

    // Data rows
    parts.push('<tbody>');
    for (let r = 0; r < rowCount; r++) {
        const row = rows[r];
        parts.push('<tr>');
        parts.push(`<td class="excel-row-num">${r + 1}</td>`);
        for (let c = 0; c < colCount; c++) {
            const val = c < row.length ? row[c] : '';
            const isNum = typeof val === 'number';
            parts.push(
                `<td${isNum ? ' class="excel-num"' : ''}>${esc(val)}</td>`
            );
        }
        parts.push('</tr>');
    }
    parts.push('</tbody></table>');

    const DOMPurify = (await import('dompurify')).default;
    return {
        html: DOMPurify.sanitize(parts.join('')),
        rowCount,
        colCount
    };
}