export function parseCsv(content) { const rows = parseRows(content); if (rows.length === 0) return []; const headers = rows[0].map((header) => header.trim()); return rows.slice(1) .filter((row) => row.some((cell) => cell.trim() !== "")) .map((row) => { const record = {}; headers.forEach((header, index) => { record[header] = row[index] ?? ""; }); return record; }); } export function stringifyCsv(records) { if (records.length === 0) return ""; const headers = [ "platform", "metric_label", "name", "url", "hotness_raw", "hotness_number", "unit", "confidence", "evidence", "status", "fetched_at", "error", ]; const lines = [headers.join(",")]; for (const record of records) { lines.push(headers.map((header) => csvEscape(record[header] ?? "")).join(",")); } return `${lines.join("\n")}\n`; } function parseRows(content) { const rows = []; let row = []; let cell = ""; let inQuotes = false; for (let i = 0; i < content.length; i += 1) { const char = content[i]; const next = content[i + 1]; if (char === "\"" && inQuotes && next === "\"") { cell += "\""; i += 1; continue; } if (char === "\"") { inQuotes = !inQuotes; continue; } if (char === "," && !inQuotes) { row.push(cell); cell = ""; continue; } if ((char === "\n" || char === "\r") && !inQuotes) { if (char === "\r" && next === "\n") i += 1; row.push(cell); rows.push(row); row = []; cell = ""; continue; } cell += char; } if (cell.length > 0 || row.length > 0) { row.push(cell); rows.push(row); } return rows; } function csvEscape(value) { const text = String(value); if (/[",\r\n]/.test(text)) { return `"${text.replace(/"/g, "\"\"")}"`; } return text; }