Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/csv-stringify/lib/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ const stringifier = function (options, state, info) {
let csvrecord = "";
for (let i = 0; i < record.length; i++) {
let options, err;
// The delimiters to quote against, unless the cast provides its own
let record_delimiters = this.state.record_delimiters;

let [value, field] = record[i];
if (typeof value === "string") {
Expand All @@ -138,11 +140,16 @@ const stringifier = function (options, state, info) {
];
}
// Merge global options with the ones returned by cast
options = { ...this.options, ...options };
[err, options] = normalize_options(options);
const cast_options = options;
options = { ...this.options, ...cast_options };
let cast_record_delimiters;
[err, options, cast_record_delimiters] = normalize_options(options);
if (err !== undefined) {
return [err];
}
if (cast_options.record_delimiter !== undefined) {
record_delimiters = cast_record_delimiters;
}
} else if (value === undefined || value === null) {
options = this.options;
} else {
Expand All @@ -160,7 +167,6 @@ const stringifier = function (options, state, info) {
quoted_empty,
quoted_string,
quoted_match,
record_delimiter,
escape_formulas,
} = options;
if ("" === value && "" === field) {
Expand Down Expand Up @@ -194,7 +200,9 @@ const stringifier = function (options, state, info) {
delimiter.length && value.indexOf(delimiter) >= 0;
const containsQuote = quote !== "" && value.indexOf(quote) >= 0;
const containsEscape = value.indexOf(escape) >= 0 && escape !== quote;
const containsRecordDelimiter = value.indexOf(record_delimiter) >= 0;
const containsRecordDelimiter = record_delimiters.some(
(record_delimiter) => value.indexOf(record_delimiter) >= 0,
);
const quotedString = quoted_string && typeof field === "string";
let quotedMatch =
quoted_match &&
Expand Down
13 changes: 12 additions & 1 deletion packages/csv-stringify/lib/api/normalize_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,13 @@ const normalize_options = function (opts) {
return [Error(`Invalid Option: "on_record" must be a function.`)];
}
// Normalize option `record_delimiter`
let record_delimiter_defaulted = false;
if (
options.record_delimiter === undefined ||
options.record_delimiter === null
) {
options.record_delimiter = "\n";
record_delimiter_defaulted = true;
} else if (Buffer.isBuffer(options.record_delimiter)) {
options.record_delimiter = options.record_delimiter.toString();
} else if (typeof options.record_delimiter !== "string") {
Expand All @@ -273,7 +275,16 @@ const normalize_options = function (opts) {
options.record_delimiter = "\u2028";
break;
}
return [undefined, options];
// The record delimiters a field must be quoted against. Left to its default,
// `parse` discovers `\r\n`, `\n` and `\r` while we only write `\n`, so a
// field holding one of the two others is split back into several records.
// Testing both characters covers the three sequences. An explicit delimiter
// is the only one a matching parser is configured with, thus the only one to
// quote against.
const record_delimiters = record_delimiter_defaulted
? ["\n", "\r"]
: [options.record_delimiter];
return [undefined, options, record_delimiters];
};

export { normalize_options };
3 changes: 2 additions & 1 deletion packages/csv-stringify/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { normalize_options } from "./api/normalize_options.js";
class Stringifier extends Transform {
constructor(opts = {}) {
super({ ...{ writableObjectMode: true }, ...opts });
const [err, options] = normalize_options(opts);
const [err, options, record_delimiters] = normalize_options(opts);
if (err !== undefined) throw err;
// Expose options
this.options = options;
// Internal state
this.state = {
stop: false,
record_delimiters: record_delimiters,
};
// Information
this.info = {
Expand Down
3 changes: 2 additions & 1 deletion packages/csv-stringify/lib/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { normalize_options } from "./api/normalize_options.js";

const stringify = function (records, opts = {}) {
const data = [];
const [err, options] = normalize_options(opts);
const [err, options, record_delimiters] = normalize_options(opts);
if (err !== undefined) throw err;
const state = {
stop: false,
record_delimiters: record_delimiters,
};
// Information
const info = {
Expand Down
3 changes: 2 additions & 1 deletion packages/csv-stringify/test/option.escape_formulas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions packages/csv-stringify/test/option.quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ describe("Option `quote`", function () {
);
});

it("quotes a field containing a carriage return", function (next) {
// The default record delimiter is "\n" but `parse` defaults to discovering
// "\r\n", "\n" and "\r", so a lone "\r" must be quoted to survive a round
// trip (it previously leaked through unquoted and split the record).
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(
[
Expand Down