import { assert } from "../_util/asserts.ts";
export interface ReadOptions { separator?: string; comment?: string; trimLeadingSpace?: boolean; lazyQuotes?: boolean; fieldsPerRecord?: number;}
export const defaultReadOptions: ReadOptions = { separator: ",", trimLeadingSpace: false,};
export interface LineReader { readLine(): Promise<string | null>; isEOF(): Promise<boolean>;}
export async function readRecord( startLine: number, reader: LineReader, opt: ReadOptions = defaultReadOptions,): Promise<string[] | null> { const line = await reader.readLine(); if (line === null) return null; if (line.length === 0) { return []; }
return parseRecord(line, reader, opt, startLine, startLine + 1);}
export async function parseRecord( line: string, reader: LineReader, opt: ReadOptions = defaultReadOptions, startLine: number, lineIndex: number = startLine,): Promise<Array<string> | null> { if (opt.comment && line[0] === opt.comment) { return []; }
assert(opt.separator != null);
let fullLine = line; let quoteError: ParseError | null = null; const quote = '"'; const quoteLen = quote.length; const separatorLen = opt.separator.length; let recordBuffer = ""; const fieldIndexes = [] as number[]; parseField: for (;;) { if (opt.trimLeadingSpace) { line = line.trimStart(); }
if (line.length === 0 || !line.startsWith(quote)) { const i = line.indexOf(opt.separator); let field = line; if (i >= 0) { field = field.substring(0, i); } if (!opt.lazyQuotes) { const j = field.indexOf(quote); if (j >= 0) { const col = runeCount( fullLine.slice(0, fullLine.length - line.slice(j).length), ); quoteError = new ParseError( startLine + 1, lineIndex, col, ERR_BARE_QUOTE, ); break parseField; } } recordBuffer += field; fieldIndexes.push(recordBuffer.length); if (i >= 0) { line = line.substring(i + separatorLen); continue parseField; } break parseField; } else { line = line.substring(quoteLen); for (;;) { const i = line.indexOf(quote); if (i >= 0) { recordBuffer += line.substring(0, i); line = line.substring(i + quoteLen); if (line.startsWith(quote)) { recordBuffer += quote; line = line.substring(quoteLen); } else if (line.startsWith(opt.separator)) { line = line.substring(separatorLen); fieldIndexes.push(recordBuffer.length); continue parseField; } else if (0 === line.length) { fieldIndexes.push(recordBuffer.length); break parseField; } else if (opt.lazyQuotes) { recordBuffer += quote; } else { const col = runeCount( fullLine.slice(0, fullLine.length - line.length - quoteLen), ); quoteError = new ParseError( startLine + 1, lineIndex, col, ERR_QUOTE, ); break parseField; } } else if (line.length > 0 || !(await reader.isEOF())) { recordBuffer += line; const r = await reader.readLine(); lineIndex++; line = r ?? ""; fullLine = line; if (r === null) { if (!opt.lazyQuotes) { const col = runeCount(fullLine); quoteError = new ParseError( startLine + 1, lineIndex, col, ERR_QUOTE, ); break parseField; } fieldIndexes.push(recordBuffer.length); break parseField; } recordBuffer += "\n"; } else { if (!opt.lazyQuotes) { const col = runeCount(fullLine); quoteError = new ParseError( startLine + 1, lineIndex, col, ERR_QUOTE, ); break parseField; } fieldIndexes.push(recordBuffer.length); break parseField; } } } } if (quoteError) { throw quoteError; } const result = [] as string[]; let preIdx = 0; for (const i of fieldIndexes) { result.push(recordBuffer.slice(preIdx, i)); preIdx = i; } return result;}
function runeCount(s: string): number { return Array.from(s).length;}
export class ParseError extends SyntaxError { startLine: number; line: number; column: number | null;
constructor( start: number, line: number, column: number | null, message: string, ) { super(); this.startLine = start; this.column = column; this.line = line;
if (message === ERR_FIELD_COUNT) { this.message = `record on line ${line}: ${message}`; } else if (start !== line) { this.message = `record on line ${start}; parse error on line ${line}, column ${column}: ${message}`; } else { this.message = `parse error on line ${line}, column ${column}: ${message}`; } }}
export const ERR_BARE_QUOTE = 'bare " in non-quoted-field';export const ERR_QUOTE = 'extraneous or missing " in quoted-field';export const ERR_INVALID_DELIM = "Invalid Delimiter";export const ERR_FIELD_COUNT = "wrong number of fields";
export function convertRowToObject( row: string[], headers: string[], index: number,) { if (row.length !== headers.length) { throw new Error( `Error number of fields line: ${index}\nNumber of fields found: ${headers.length}\nExpected number of fields: ${row.length}`, ); } const out: Record<string, unknown> = {}; for (let i = 0; i < row.length; i++) { out[headers[i]] = row[i]; } return out;}
export type RowType<ParseOptions, T> = T extends Omit<ParseOptions, "columns"> & { columns: string[] } ? Record<string, unknown> : T extends Omit<ParseOptions, "skipFirstRow"> & { skipFirstRow: true } ? Record<string, unknown> : string[];