diff --git a/packages/csv-stringify/lib/api/index.js b/packages/csv-stringify/lib/api/index.js index 0f214501..0f2a089b 100644 --- a/packages/csv-stringify/lib/api/index.js +++ b/packages/csv-stringify/lib/api/index.js @@ -195,6 +195,17 @@ const stringifier = function (options, state, info) { const containsQuote = quote !== "" && value.indexOf(quote) >= 0; const containsEscape = value.indexOf(escape) >= 0 && escape !== quote; const containsRecordDelimiter = value.indexOf(record_delimiter) >= 0; + // With a newline-based record delimiter, a field containing either + // line-break character must be quoted: the parser treats both `\r` + // and `\n` as record delimiters, so an unquoted value (e.g. a lone + // `\r` when the delimiter is `\n`) would be split apart on the next + // parse. A custom, non-newline delimiter leaves line breaks as data. + const recordDelimiterIsNewline = + record_delimiter.indexOf("\n") >= 0 || + record_delimiter.indexOf("\r") >= 0; + const containsLineBreak = + recordDelimiterIsNewline && + (value.indexOf("\r") >= 0 || value.indexOf("\n") >= 0); const quotedString = quoted_string && typeof field === "string"; let quotedMatch = quoted_match && @@ -232,6 +243,7 @@ const stringifier = function (options, state, info) { containsQuote === true || containsdelimiter || containsRecordDelimiter || + containsLineBreak || quoted || quotedString || quotedMatch; diff --git a/packages/csv-stringify/test/option.escape_formulas.ts b/packages/csv-stringify/test/option.escape_formulas.ts index 9963c653..c371c36b 100644 --- a/packages/csv-stringify/test/option.escape_formulas.ts +++ b/packages/csv-stringify/test/option.escape_formulas.ts @@ -39,7 +39,8 @@ describe("Option `escape_formulas`", function () { "'-c,3", "'@d,4", "'\te,5", - "'\rf,6", + // The carriage return forces quoting so the field round-trips. + "\"'\rf\",6", "g,7", "'\uFF1Dh,8", "'\uFF0Bi,9", diff --git a/packages/csv-stringify/test/option.quote.ts b/packages/csv-stringify/test/option.quote.ts index 851b3150..7ca2a919 100644 --- a/packages/csv-stringify/test/option.quote.ts +++ b/packages/csv-stringify/test/option.quote.ts @@ -184,6 +184,16 @@ describe("Option `quote`", function () { ); }); + it("quotes a field containing a carriage return", function (next) { + // A lone "\r" is treated as a record delimiter by the parser, so it must + // be quoted to survive a round trip (it previously leaked through unquoted). + stringify([["a\rb"]], { eof: false }, (err, data) => { + if (err) return next(err); + data.should.eql('"a\rb"'); + next(); + }); + }); + it("field where quote string is empty", function (next) { stringify( [