From 366d8de3f3983e97bc78dc5fd7125050ed1bfd47 Mon Sep 17 00:00:00 2001 From: sunlishuo Date: Sun, 4 Oct 2026 21:05:46 +0800 Subject: [PATCH] fix(csv-stringify): quote empty strings returned by casts --- packages/csv-stringify/lib/api/index.js | 6 ++-- .../csv-stringify/test/option.quoted_match.ts | 25 ++++++++++++++ .../test/option.quoted_string.ts | 33 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/csv-stringify/lib/api/index.js b/packages/csv-stringify/lib/api/index.js index 1fb77be2b..9c78ea91b 100644 --- a/packages/csv-stringify/lib/api/index.js +++ b/packages/csv-stringify/lib/api/index.js @@ -194,12 +194,14 @@ const stringifier = function (options, state, info) { quote_record_delimiter, escape_formulas, } = options; - if ("" === value && "" === field) { + if ("" === value) { const quotedMatch = matches_quoted_match(value, quoted_match); const shouldQuote = quotedMatch || true === quoted_empty || - (true === quoted_string && false !== quoted_empty); + (true === quoted_string && + typeof field === "string" && + false !== quoted_empty); if (shouldQuote === true) { value = quote + value + quote; } diff --git a/packages/csv-stringify/test/option.quoted_match.ts b/packages/csv-stringify/test/option.quoted_match.ts index 3770d1a3a..d9032923a 100644 --- a/packages/csv-stringify/test/option.quoted_match.ts +++ b/packages/csv-stringify/test/option.quoted_match.ts @@ -1,8 +1,33 @@ import "should"; import should from "should"; import { stringify } from "../lib/index.js"; +import { stringify as stringifySync } from "../lib/sync.js"; describe("Option `quoted_match`", function () { + it("matches an empty string returned by a string cast", function (next) { + stringify( + [[" ", "", null, undefined, "text"]], + { quoted_match: /^$/, cast: { string: (value) => value.trim() } }, + (err, data) => { + if (err) return next(err); + data.should.eql('"","",,,text\n'); + next(); + }, + ); + }); + + it("matches empty cast values in the sync API and local options", function () { + const data = stringifySync([[" ", 0, {}, null, undefined]], { + quoted_match: /^$/, + cast: { + string: (value) => value.trim(), + number: () => "", + object: () => ({ value: "", quoted_match: "" }), + }, + }); + data.should.eql('"","","",,\n'); + }); + it("default to `null`", function (next) { const stringifier = stringify([["abc", "def"]], () => { should(stringifier.options.quoted_match).be.null(); diff --git a/packages/csv-stringify/test/option.quoted_string.ts b/packages/csv-stringify/test/option.quoted_string.ts index f3ee7ca75..c653befc8 100644 --- a/packages/csv-stringify/test/option.quoted_string.ts +++ b/packages/csv-stringify/test/option.quoted_string.ts @@ -1,7 +1,40 @@ import "should"; import { stringify } from "../lib/index.js"; +import { stringify as stringifySync } from "../lib/sync.js"; describe("Option `quoted_string`", function () { + it("quotes a string cast to an empty string", function (next) { + stringify( + [[" ", "", null, undefined, "text"]], + { quoted_string: true, cast: { string: (value) => value.trim() } }, + (err, data) => { + if (err) return next(err); + data.should.eql('"","",,,"text"\n'); + next(); + }, + ); + }); + + it("only quotes string fields when casts return empty strings", function () { + const data = stringifySync([[" ", "", 0, false, null, undefined]], { + quoted_string: true, + cast: { + string: (value) => value.trim(), + number: () => "", + }, + }); + data.should.eql('"","",,,,\n'); + }); + + it("respects quoted_empty when strings are cast to empty strings", function () { + const data = stringifySync([[" ", ""]], { + quoted_string: true, + quoted_empty: false, + cast: { string: (value) => value.trim() }, + }); + data.should.eql(",\n"); + }); + it("quotes string fields", function (next) { stringify( [[undefined, null, "", " ", "x", 0, false]],