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(); + }); + }); });