Your Quoted CSV Fields Are Being Trimmed
The rule everyone assumes, and what happens here
RFC 4180 §2.6: a field enclosed in double quotes may contain line breaks, commas, and quotes — and the quotes exist precisely so those characters survive untouched. So " pending " should give you " pending ".
Run through the shipped parser:
IN id,note
1," pending "
OUT [{ "id": "1", "note": "pending" }]
The whitespace is gone. Every field in the output — quoted or not — passes through String.prototype.trim() before you ever see it. There is no option to turn this off, because nothing in the app exposes an option at all.
When does that cost you something real? Leading and trailing whitespace is meaningful in the data people actually hand you: a status code that downstream string-compares against "NEW ", an account number with a trailing space used as a join key, an address line where the space was the only separator, a fixed-width export whose padding was the format. Trim-and-compare is also exactly how two different values become the same value, and the tool gives you no signal that it happened — 1 rows, no warning, identical-looking output.
The fix is not to trust the round trip. If whitespace matters, verify it survived, or do the conversion with a parser that has a skip_initial_space / trim switch you can set explicitly. The rest of this page is about which parts of quoting this parser does honour, so you can tell the difference between "fine" and "quietly rewritten".
Rule 1 — a quote only opens a field if it starts the field
if (c === '"' && field === "") { inQuotes = true; wasQuoted = true; continue; }
That field === "" test is correct and it is also where two behaviours come from.
Good: mid-field quotes stay literal. a,"b"c,d keeps "b"c as written rather than inventing a field boundary.
The edge that bites: a quote preceded by a space never opens quoting, because by then field is " ", not empty.
IN a,b
x, "y,z"
OUT [{ "a": "x", "b": "\"y" }]
STAT 1 rows — WARNING: 1 row(s) have a different column count than the header
Excel and every spreadsheet export writes "y,z" immediately after the comma with no space, so this is safe for real exports. Hand-typed or sed-poked CSV gets a field cut in half at the inner comma and the columns shift. The warning is the saving grace here — this is the loud quoting failure, and it is worth knowing which ones are loud so you can tell them from the quiet ones below.
Rule 2 — doubled quotes unescape, correctly
"" inside a quoted field becomes one " and the field keeps being quoted. Standard, working, no caveats. It is also the reason wasQuoted exists in the code: a field containing only "" must survive as an empty-but-present field rather than being filtered out as a blank line.
IN a,b
"",x
OUT [{ "a": "", "b": "x" }]
Correct, and more than a few hand-rolled parsers get it.
Rule 3 — an unterminated quote swallows the rest of the file
IN a,b
1,"open
2,next
OUT [{ "a": "1", "b": "open\n2,next" }]
STAT 1 rows
One missing quote at row 1 turns rows 2 and 3 into a single cell containing a newline and a comma. Two data rows became one record and the row counter says 1 rows with no warning at all — the ragged check cannot help, because the output genuinely has 1 row and 2 columns, which matches the header.
This is the worst failure mode on the page: structurally valid output, wrong shape, no signal. The row-count statistic is your only clue and it reads like a fact. Check it against how many rows you expect. A 1 rows under a file you know has 40 lines is the entire alarm.
The same thing happens with a trailing newline inside quotes — "line\n" loses its newline and the file still reports clean.
Rule 4 — blank-line handling is asymmetric
Truly empty lines are dropped, which is what you want after a trailing newline:
IN a IN a
1 1
(blank) (blank)
2 2
OUT 1,2 OUT 2 rows
A line containing only spaces is not blank, and becomes a real record:
IN a,b
,
OUT [{ "a": "", "b": "" }] STAT 1 rows
So , in a file produces a phantom row of empty values that will happily insert into a table. Delete-row logic keyed on "empty" will catch it; logic keyed on "row exists" will not.
No dialect detection, and no warning about it
The comma is hardcoded in parseCsv. Semicolon and tab files are not rejected — they are parsed as single-column CSV, confidently:
IN a;b OUT [{ "a;b": "1;2" }] STAT 1 rows
1;2
IN a⇥b OUT [{ "a⇥b": "1⇥2" }] STAT 2 rows (tab-delimited, read as 1 column)
The header is the giveaway: a header containing ; or a tab means your delimiter is wrong, not that you have one beautiful column. Nothing here checks for that, because checking would mean admitting a delimiter is a user choice — which this tool has decided it isn't. For European exports (semicolon is the default in Excel for most of Europe, because comma is the decimal separator) this silently produces garbage every time.
One thing it gets right that surprises people
The UTF-8 BOM is stripped. Excel writes \uFEFF at the start of every CSV-it-saves-as-"CSV UTF-8", and the classic bug is a first header named \uFEFFid, which breaks every obj.id lookup downstream:
IN <BOM>id,name OUT [{ "id": "1", "name": "ann" }]
1,ann
id is clean. This works by accident — trim() removes U+FEFF because it counts as whitespace — but it works, and it is the single most common CSV-in-JavaScript bug, so worth stating.
How to re-run this
All output above came from the shipped csvToJson, loaded unchanged from the deployed dist/index.html by the repo's fuzz harness — not a reimplementation:
node -e 'process.env.FUZZ_APP_ROOT="/tmp/fuzzroot";
const {loadApp}=require("./scripts/fuzz/load.js");
const A=loadApp("csvformat",["csvToJson"]).api;
A.__setInput("in","id,note\n1,\" pending \"");
A.__setInput("out","");
A.csvToJson();
console.log(A.__readOutput("out"));
console.log(A.__readOutput("in-stats"));'
# → [{ "id": "1", "note": "pending" }]
# → 1 rows
Not verified
I did not test this parser against files with mixed line endings in a single record (a bare \r with no \n is treated as ordinary data — I have not confirmed what that looks like in output for a \r-only legacy Mac file), nor against multi-gigabyte input, nor with a BOM appearing anywhere other than position 0. The 1 rows figure for the swallowed-quote case is the app's own row count, which is what a user sees; I did not separately verify how it behaves when the unclosed quote is the last field of the last row.