From 222550c7451c7807e8d0c5b473eb309985d85b32 Mon Sep 17 00:00:00 2001 From: greymoth <246701683+greymoth-jp@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:19:25 +0900 Subject: [PATCH] fix(csv-stringify): quote fields containing a bare CR or LF A field holding a carriage return or line feed is left unquoted unless it contains the configured record_delimiter (default "\n"), so a lone "\r" fails to round-trip: parse auto-detects CR as a record delimiter and reports CSV_RECORD_INCONSISTENT_FIELDS_LENGTH. Quote any field that contains CR or LF. --- packages/csv-stringify/lib/api/index.js | 9 +++++++++ packages/csv-stringify/test/option.record_delimiter.js | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/csv-stringify/lib/api/index.js b/packages/csv-stringify/lib/api/index.js index 39de6053..bbfff9e4 100644 --- a/packages/csv-stringify/lib/api/index.js +++ b/packages/csv-stringify/lib/api/index.js @@ -212,6 +212,14 @@ const stringifier = function (options, state, info) { value, record_delimiter, ); + // `parse` treats CR, LF and CRLF as record delimiters when + // `record_delimiter` is left to auto-detection (its default), so a + // field holding a bare CR or LF starts a new record and must be + // quoted to round-trip, even when it does not contain the configured + // `record_delimiter` (eg a lone "\r" stringified with the default + // "\n" record delimiter). + const containsNewline = + value.indexOf("\r") !== -1 || value.indexOf("\n") !== -1; const quotedString = quoted_string && typeof field === "string"; let quotedMatch = quoted_match && @@ -249,6 +257,7 @@ const stringifier = function (options, state, info) { containsQuote === true || containsdelimiter || containsRecordDelimiter || + containsNewline || quoted || quotedString || quotedMatch; diff --git a/packages/csv-stringify/test/option.record_delimiter.js b/packages/csv-stringify/test/option.record_delimiter.js index 44181a6f..1a399a9a 100644 --- a/packages/csv-stringify/test/option.record_delimiter.js +++ b/packages/csv-stringify/test/option.record_delimiter.js @@ -31,4 +31,14 @@ describe("Option `record_delimiter`", function () { }, ); }); + it("quote a field containing a bare carriage return", function (next) { + // `parse` treats a lone CR as a record delimiter under its default + // auto-detection, so a field holding a CR must be quoted to round-trip even + // though it does not contain the default "\n" record delimiter. + stringify([["x\ry", "z"]], { eof: false }, (err, data) => { + if (err) return next(err); + data.toString().should.eql('"x\ry",z'); + next(); + }); + }); });