csvToMap method
Parses CSV text into a row-indexed map. The parser is RFC 4180
compliant: it preserves fields exactly (no whitespace trimming),
supports both \n and \r\n line terminators, supports quoted
fields that contain commas, quotes (escaped as "") and newlines,
and does NOT silently renumber rows when the input contains blank
lines — blank lines are preserved as empty rows so row indices match
the source.
Implementation
Map<int, List<String>> csvToMap(String input) {
final rows = <List<String>>[];
var field = StringBuffer();
var row = <String>[];
var inQuotes = false;
var i = 0;
final n = input.length;
while (i < n) {
final ch = input[i];
if (inQuotes) {
if (ch == '"') {
if (i + 1 < n && input[i + 1] == '"') {
// Escaped quote inside a quoted field.
field.write('"');
i += 2;
continue;
}
inQuotes = false;
i++;
continue;
}
field.write(ch);
i++;
continue;
}
if (ch == '"') {
inQuotes = true;
i++;
continue;
}
if (ch == ',') {
row.add(field.toString());
field = StringBuffer();
i++;
continue;
}
if (ch == '\r') {
// Consume CR (and an optional following LF) as a row terminator.
row.add(field.toString());
rows.add(row);
field = StringBuffer();
row = <String>[];
if (i + 1 < n && input[i + 1] == '\n') {
i += 2;
} else {
i++;
}
continue;
}
if (ch == '\n') {
row.add(field.toString());
rows.add(row);
field = StringBuffer();
row = <String>[];
i++;
continue;
}
field.write(ch);
i++;
}
// Flush the final field/row if input did not end with a line terminator.
// If the input ended cleanly with a terminator and there's no pending
// content, we don't emit a phantom trailing empty row.
if (field.isNotEmpty || row.isNotEmpty) {
row.add(field.toString());
rows.add(row);
}
final res = <int, List<String>>{};
for (var r = 0; r < rows.length; r++) {
res[r] = rows[r];
}
return res;
}