Repository Public
1// A real CSV reader: quoted fields, doubled quotes, separators and line breaks inside quotes, CR/LF/CRLF.23/** The separator that splits the first lines most consistently (commas, semicolons or tabs outside quotes). */4export function detectDelimiter(text: string): ',' | ';' | '\t' {5 const candidates = [',', ';', '\t'] as const;6 const perLine: Record<string, number[]> = { ',': [], ';': [], '\t': [] };7 let quoted = false, lines = 0;8 const counts: Record<string, number> = { ',': 0, ';': 0, '\t': 0 };9 const flush = () => { for (const c of candidates) perLine[c]!.push(counts[c]!); counts[','] = counts[';'] = counts['\t'] = 0; lines++; };10 for (let i = 0; i < text.length && lines < 20 && i < 200_000; i++) {11 const ch = text[i]!;12 if (ch === '"') quoted = !quoted;13 else if (!quoted && (ch === '\n' || ch === '\r')) { if (ch === '\r' && text[i + 1] === '\n') i++; flush(); }14 else if (!quoted && ch in counts) counts[ch]!++;15 }16 if (counts[','] || counts[';'] || counts['\t'] || lines === 0) flush();17 let best: ',' | ';' | '\t' = ',', bestScore = 0;18 for (const c of candidates) {19 const xs = perLine[c]!.filter((n, i, a) => n > 0 || i < a.length - 1);20 const total = xs.reduce((a, b) => a + b, 0);21 if (!total) continue;22 const same = xs.filter((n) => n === xs[0]).length / xs.length; // a real separator appears the same number of times on each line23 const score = total * (0.25 + same);24 if (score > bestScore) { best = c; bestScore = score; }25 }26 return best;27}2829export interface CsvTable { rows: string[][]; delimiter: string; more: boolean; columns: number }30/** Parse up to `maxRows` records; `more` tells that the text held further ones. */31export function parseCsv(text: string, maxRows: number, delimiter: string = detectDelimiter(text)): CsvTable {32 const rows: string[][] = [];33 let row: string[] = [], field = '', quoted = false, i = text.charCodeAt(0) === 0xfeff ? 1 : 0, columns = 0, more = false, started = false;34 const endRow = () => { row.push(field); field = ''; if (row.length > 1 || row[0] !== '' ) { rows.push(row); columns = Math.max(columns, row.length); } row = []; started = false; };35 for (; i < text.length; i++) {36 const ch = text[i]!;37 if (quoted) {38 if (ch === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else quoted = false; }39 else field += ch;40 continue;41 }42 if (ch === '"' && field === '') { quoted = true; started = true; }43 else if (ch === delimiter) { row.push(field); field = ''; started = true; }44 else if (ch === '\n' || ch === '\r') {45 if (ch === '\r' && text[i + 1] === '\n') i++;46 endRow();47 if (rows.length >= maxRows) { more = /\S/.test(text.slice(i + 1, i + 4096)); break; }48 } else { field += ch; started = true; }49 }50 if (!more && (started || field !== '' || row.length)) endRow();51 return { rows, delimiter, more, columns };52}53