/** * Minimal RFC 4180 CSV parser for database CSV import (T-165). * * Zero dependencies (license-review rule): handles quoted fields, escaped * `""` quotes, and embedded commas / CRLF newlines inside quotes. The row * length is NOT fixed to the header (short rows pad with "", long rows keep * their extras) so a ragged file still maps positionally — the caller * decides what to do with unmatched columns. */ const MAX_CELLS_PER_ROW = 1000; export function parseCsv(input: string): string[][] { // Tolerate a UTF-8 BOM from spreadsheet exports. const text = input.charCodeAt(0) === 0xfeff ? input.slice(1) : input; const rows: string[][] = []; let row: string[] = []; let cell = ""; let inQuotes = false; for (let i = 0; i < text.length; i += 1) { const c = text[i]; if (inQuotes) { if (c === '"') { if (text[i + 1] === '"') { cell += '"'; i += 1; } else { inQuotes = false; } } else { cell += c; } continue; } if (c === '"') { inQuotes = true; } else if (c === ",") { row.push(cell); cell = ""; } else if (c === "\n" || c === "\r") { if (c === "\r" && text[i + 1] === "\n") i += 1; row.push(cell); cell = ""; rows.push(row); row = []; } else { cell += c; } } // Unterminated quote: keep what was collected (the dry run reports it). if (cell.length > 0 || row.length > 0) { row.push(cell); rows.push(row); } return rows.map((r) => (r.length > MAX_CELLS_PER_ROW ? r.slice(0, MAX_CELLS_PER_ROW) : r)); }