utilful 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/csv.d.mts +76 -1
- package/dist/csv.mjs +228 -72
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/json.mjs +4 -4
- package/package.json +1 -1
package/dist/csv.d.mts
CHANGED
|
@@ -55,6 +55,41 @@ interface CSVCreateOptions {
|
|
|
55
55
|
declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], columns: readonly (keyof T)[], options?: CSVCreateOptions): string;
|
|
56
56
|
declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], options?: CSVCreateOptions): string;
|
|
57
57
|
/**
|
|
58
|
+
* Creates a CSV stream from an iterable or async iterable of objects.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* This function yields CSV content as strings, including line endings.
|
|
62
|
+
* Each yielded chunk contains complete lines (header and/or data rows).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const data = [
|
|
66
|
+
* { name: 'John', age: '30' },
|
|
67
|
+
* { name: 'Jane', age: '25' }
|
|
68
|
+
* ]
|
|
69
|
+
*
|
|
70
|
+
* for await (const chunk of createCSVStream(data, ['name', 'age'])) {
|
|
71
|
+
* console.log(chunk)
|
|
72
|
+
* }
|
|
73
|
+
*/
|
|
74
|
+
declare function createCSVStream<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): AsyncIterable<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Creates a CSV string from an async iterable or iterable of objects.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* This is a convenience wrapper around `createCSVStream` that collects
|
|
80
|
+
* all chunks into a single string. Note that the result will have a
|
|
81
|
+
* trailing line ending, unlike the synchronous `createCSV`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const data = [
|
|
85
|
+
* { name: 'John', age: '30' },
|
|
86
|
+
* { name: 'Jane', age: '25' }
|
|
87
|
+
* ]
|
|
88
|
+
*
|
|
89
|
+
* const csv = await createCSVAsync(data, ['name', 'age'])
|
|
90
|
+
*/
|
|
91
|
+
declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): Promise<string>;
|
|
92
|
+
/**
|
|
58
93
|
* Escapes a value for a CSV string.
|
|
59
94
|
*
|
|
60
95
|
* @remarks
|
|
@@ -100,5 +135,45 @@ declare function parseCSV<Header extends string>(csv?: string | null | undefined
|
|
|
100
135
|
*/
|
|
101
136
|
strict?: boolean;
|
|
102
137
|
}): CSVRow<Header>[];
|
|
138
|
+
/**
|
|
139
|
+
* Parses CSV data from an async iterable or iterable of string chunks.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* This function yields CSV rows as they are parsed. Chunks do not need to
|
|
143
|
+
* align with row boundaries; the parser handles quotes and newlines correctly
|
|
144
|
+
* across chunk boundaries.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const chunks = ['name,age\nJo', 'hn,30\nJane,25']
|
|
148
|
+
*
|
|
149
|
+
* for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
|
|
150
|
+
* console.log(row)
|
|
151
|
+
* }
|
|
152
|
+
*/
|
|
153
|
+
declare function parseCSVStream<Header extends string>(chunks: AsyncIterable<string> | Iterable<string>, options?: {
|
|
154
|
+
delimiter?: string;
|
|
155
|
+
trim?: boolean;
|
|
156
|
+
strict?: boolean;
|
|
157
|
+
}): AsyncIterable<CSVRow<Header>>;
|
|
158
|
+
/**
|
|
159
|
+
* Parses CSV data from an async iterable or iterable of lines.
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* This is a convenience wrapper around `parseCSVStream` that treats each line
|
|
163
|
+
* as a chunk. Note that lines do not necessarily correspond to CSV rows due to
|
|
164
|
+
* quoted fields containing newlines. The parser handles this correctly.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* const lines = ['name,age', 'John,30', 'Jane,25']
|
|
168
|
+
*
|
|
169
|
+
* for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
|
|
170
|
+
* console.log(row)
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
declare function parseCSVFromLines<Header extends string>(lines: AsyncIterable<string> | Iterable<string>, options?: {
|
|
174
|
+
delimiter?: string;
|
|
175
|
+
trim?: boolean;
|
|
176
|
+
strict?: boolean;
|
|
177
|
+
}): AsyncIterable<CSVRow<Header>>;
|
|
103
178
|
//#endregion
|
|
104
|
-
export { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV };
|
|
179
|
+
export { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
|
package/dist/csv.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
//#region src/csv.ts
|
|
2
|
+
const COMMA = ",";
|
|
3
|
+
const DOUBLE_QUOTE = "\"";
|
|
4
|
+
const NEWLINE = "\n";
|
|
5
|
+
const CARRIAGE_RETURN = "\r";
|
|
6
|
+
const ESCAPED_QUOTE = "\"\"";
|
|
7
|
+
const SPACE = " ";
|
|
8
|
+
const TAB = " ";
|
|
2
9
|
function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
3
10
|
let columns;
|
|
4
11
|
let options;
|
|
@@ -10,19 +17,71 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
|
10
17
|
options = columnsOrOptions ?? {};
|
|
11
18
|
}
|
|
12
19
|
if (columns.length === 0 && data.length === 0) return "";
|
|
13
|
-
const { delimiter =
|
|
20
|
+
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
14
21
|
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
15
|
-
const formatCell = (value) => escapeCSVValue(value, {
|
|
16
|
-
delimiter,
|
|
17
|
-
quoteAll
|
|
18
|
-
});
|
|
19
|
-
const rows = data.map((obj) => columns.map((key) => formatCell(obj[key])).join(delimiter));
|
|
20
22
|
if (addHeader) {
|
|
21
|
-
const header = columns.map(
|
|
22
|
-
if (
|
|
23
|
-
|
|
23
|
+
const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
|
|
24
|
+
if (data.length === 0) return header;
|
|
25
|
+
const bodyLines = data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll));
|
|
26
|
+
return header + lineEnding + bodyLines.join(lineEnding);
|
|
24
27
|
}
|
|
25
|
-
return
|
|
28
|
+
return data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll)).join(lineEnding);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a CSV stream from an iterable or async iterable of objects.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* This function yields CSV content as strings, including line endings.
|
|
35
|
+
* Each yielded chunk contains complete lines (header and/or data rows).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const data = [
|
|
39
|
+
* { name: 'John', age: '30' },
|
|
40
|
+
* { name: 'Jane', age: '25' }
|
|
41
|
+
* ]
|
|
42
|
+
*
|
|
43
|
+
* for await (const chunk of createCSVStream(data, ['name', 'age'])) {
|
|
44
|
+
* console.log(chunk)
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
async function* createCSVStream(data, columns, options = {}) {
|
|
48
|
+
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
49
|
+
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
50
|
+
if (addHeader) yield encodeCSVHeader(columns.map(String), delimiter, quoteAll) + lineEnding;
|
|
51
|
+
for await (const row of data) yield encodeCSVRow(row, columns, delimiter, quoteAll) + lineEnding;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a CSV string from an async iterable or iterable of objects.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* This is a convenience wrapper around `createCSVStream` that collects
|
|
58
|
+
* all chunks into a single string. Note that the result will have a
|
|
59
|
+
* trailing line ending, unlike the synchronous `createCSV`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const data = [
|
|
63
|
+
* { name: 'John', age: '30' },
|
|
64
|
+
* { name: 'Jane', age: '25' }
|
|
65
|
+
* ]
|
|
66
|
+
*
|
|
67
|
+
* const csv = await createCSVAsync(data, ['name', 'age'])
|
|
68
|
+
*/
|
|
69
|
+
async function createCSVAsync(data, columns, options = {}) {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
for await (const chunk of createCSVStream(data, columns, options)) chunks.push(chunk);
|
|
72
|
+
return chunks.join("");
|
|
73
|
+
}
|
|
74
|
+
function encodeCSVHeader(columns, delimiter, quoteAll) {
|
|
75
|
+
return columns.map((col) => escapeCSVValue(col, {
|
|
76
|
+
delimiter,
|
|
77
|
+
quoteAll
|
|
78
|
+
})).join(delimiter);
|
|
79
|
+
}
|
|
80
|
+
function encodeCSVRow(row, columns, delimiter, quoteAll) {
|
|
81
|
+
return columns.map((key) => escapeCSVValue(row[key], {
|
|
82
|
+
delimiter,
|
|
83
|
+
quoteAll
|
|
84
|
+
})).join(delimiter);
|
|
26
85
|
}
|
|
27
86
|
/**
|
|
28
87
|
* Escapes a value for a CSV string.
|
|
@@ -37,12 +96,118 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
|
37
96
|
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
38
97
|
*/
|
|
39
98
|
function escapeCSVValue(value, options = {}) {
|
|
40
|
-
const { delimiter =
|
|
99
|
+
const { delimiter = COMMA, quoteAll = false } = options;
|
|
41
100
|
if (value == null) return "";
|
|
42
101
|
const coercedValue = String(value);
|
|
43
|
-
|
|
102
|
+
const hasQuote = coercedValue.includes(DOUBLE_QUOTE);
|
|
103
|
+
if (quoteAll || coercedValue.includes(delimiter) || hasQuote || coercedValue.includes(NEWLINE) || coercedValue.includes(CARRIAGE_RETURN)) return `${DOUBLE_QUOTE}${hasQuote ? coercedValue.replaceAll(DOUBLE_QUOTE, ESCAPED_QUOTE) : coercedValue}${DOUBLE_QUOTE}`;
|
|
44
104
|
return coercedValue;
|
|
45
105
|
}
|
|
106
|
+
var CSVParserCore = class {
|
|
107
|
+
delimiter;
|
|
108
|
+
trim;
|
|
109
|
+
strict;
|
|
110
|
+
onRow;
|
|
111
|
+
currentRow = [];
|
|
112
|
+
currentRowQuotedFlags = [];
|
|
113
|
+
currentField = "";
|
|
114
|
+
inQuotes = false;
|
|
115
|
+
isFieldQuoted = false;
|
|
116
|
+
currentRowNumber = 1;
|
|
117
|
+
headerRaw;
|
|
118
|
+
headerQuotedFlags;
|
|
119
|
+
headers;
|
|
120
|
+
constructor(options, onRow) {
|
|
121
|
+
const { delimiter = COMMA, trim = true, strict = true } = options;
|
|
122
|
+
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
123
|
+
this.delimiter = delimiter;
|
|
124
|
+
this.trim = trim;
|
|
125
|
+
this.strict = strict;
|
|
126
|
+
this.onRow = onRow;
|
|
127
|
+
}
|
|
128
|
+
push(chunk) {
|
|
129
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
130
|
+
const character = chunk[i];
|
|
131
|
+
const nextCharacter = i + 1 < chunk.length ? chunk[i + 1] : "";
|
|
132
|
+
if (this.isFieldQuoted && !this.inQuotes && character !== this.delimiter && (character === SPACE || character === TAB)) continue;
|
|
133
|
+
if (character === DOUBLE_QUOTE) if (this.currentField.length === 0 && !this.inQuotes) {
|
|
134
|
+
this.inQuotes = true;
|
|
135
|
+
this.isFieldQuoted = true;
|
|
136
|
+
} else if (this.inQuotes && nextCharacter === DOUBLE_QUOTE) {
|
|
137
|
+
this.currentField += DOUBLE_QUOTE;
|
|
138
|
+
i++;
|
|
139
|
+
} else if (this.inQuotes) this.inQuotes = false;
|
|
140
|
+
else this.currentField += character;
|
|
141
|
+
else if (character === this.delimiter && !this.inQuotes) this.appendField();
|
|
142
|
+
else if ((character === NEWLINE || character === CARRIAGE_RETURN) && !this.inQuotes) {
|
|
143
|
+
if (character === CARRIAGE_RETURN && nextCharacter === NEWLINE) i++;
|
|
144
|
+
this.appendRow();
|
|
145
|
+
} else this.currentField += character;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finish() {
|
|
149
|
+
if (this.inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${this.currentRowNumber}`);
|
|
150
|
+
if (this.currentField !== "" || this.currentRow.length > 0) this.appendRow();
|
|
151
|
+
}
|
|
152
|
+
appendField() {
|
|
153
|
+
this.currentRow.push(this.currentField);
|
|
154
|
+
this.currentRowQuotedFlags.push(this.isFieldQuoted);
|
|
155
|
+
this.currentField = "";
|
|
156
|
+
this.isFieldQuoted = false;
|
|
157
|
+
}
|
|
158
|
+
appendRow() {
|
|
159
|
+
this.appendField();
|
|
160
|
+
if (!this.headerRaw) {
|
|
161
|
+
this.headerRaw = this.currentRow;
|
|
162
|
+
this.headerQuotedFlags = this.currentRowQuotedFlags;
|
|
163
|
+
this.processHeaderRow();
|
|
164
|
+
} else this.processDataRow(this.currentRow, this.currentRowQuotedFlags);
|
|
165
|
+
this.currentRow = [];
|
|
166
|
+
this.currentRowQuotedFlags = [];
|
|
167
|
+
this.currentRowNumber++;
|
|
168
|
+
}
|
|
169
|
+
processHeaderRow() {
|
|
170
|
+
const headerRow = this.headerRaw;
|
|
171
|
+
const headerQuotedFlags = this.headerQuotedFlags ?? [];
|
|
172
|
+
const headers = this.trim ? headerRow.map((h, i) => headerQuotedFlags[i] ? h : h.trim()) : headerRow;
|
|
173
|
+
if (headers.filter((h) => h.length === 0).length > 0) {
|
|
174
|
+
const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
|
|
175
|
+
throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
|
|
176
|
+
}
|
|
177
|
+
const headerSet = /* @__PURE__ */ new Set();
|
|
178
|
+
const duplicateHeaderNames = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
|
|
180
|
+
else headerSet.add(header);
|
|
181
|
+
if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
|
|
182
|
+
this.headers = headers;
|
|
183
|
+
}
|
|
184
|
+
processDataRow(fieldValues, quotedFlags) {
|
|
185
|
+
if (!this.headers) throw new Error("CSVParserCore: headers not initialized");
|
|
186
|
+
const headers = this.headers;
|
|
187
|
+
const isFieldPopulated = this.trim ? (field, wasQuoted) => wasQuoted ? field.length > 0 : field.trim().length > 0 : (field) => field.length > 0;
|
|
188
|
+
const hasMultipleFields = fieldValues.length > 1;
|
|
189
|
+
const hasAnyPopulatedField = fieldValues.some((field, idx) => isFieldPopulated(field, quotedFlags[idx] ?? false));
|
|
190
|
+
if (!hasMultipleFields && !hasAnyPopulatedField) return;
|
|
191
|
+
if (fieldValues.length > headers.length) {
|
|
192
|
+
const fieldsExceedingHeaders = fieldValues.slice(headers.length);
|
|
193
|
+
const excessQuotedFlags = quotedFlags.slice(headers.length);
|
|
194
|
+
const containsNonEmptyOverflow = fieldsExceedingHeaders.some((field, idx) => isFieldPopulated(field, excessQuotedFlags[idx] ?? false));
|
|
195
|
+
if (this.strict && containsNonEmptyOverflow) {
|
|
196
|
+
const expectedCount = headers.length;
|
|
197
|
+
const actualCount = fieldValues.length;
|
|
198
|
+
const excessCount = actualCount - expectedCount;
|
|
199
|
+
throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const rowEntries = headers.map((header, columnIndex) => {
|
|
203
|
+
const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
|
|
204
|
+
const wasQuoted = quotedFlags[columnIndex] ?? false;
|
|
205
|
+
return [header, this.trim && !wasQuoted ? untrimmedValue.trim() : untrimmedValue];
|
|
206
|
+
});
|
|
207
|
+
const rowObject = Object.fromEntries(rowEntries);
|
|
208
|
+
this.onRow(rowObject);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
46
211
|
/**
|
|
47
212
|
* Parses a comma-separated values (CSV) string into an array of objects.
|
|
48
213
|
*
|
|
@@ -60,66 +225,57 @@ function escapeCSVValue(value, options = {}) {
|
|
|
60
225
|
function parseCSV(csv, options = {}) {
|
|
61
226
|
if (!csv?.trim()) return [];
|
|
62
227
|
const rows = [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let inQuotes = false;
|
|
66
|
-
let currentRowNumber = 1;
|
|
67
|
-
const { delimiter = ",", trim = true, strict = true } = options;
|
|
68
|
-
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
69
|
-
const appendField = () => {
|
|
70
|
-
currentRow.push(currentField);
|
|
71
|
-
currentField = "";
|
|
72
|
-
};
|
|
73
|
-
const appendRow = () => {
|
|
74
|
-
appendField();
|
|
75
|
-
rows.push(currentRow);
|
|
76
|
-
currentRow = [];
|
|
77
|
-
};
|
|
78
|
-
for (let i = 0; i < csv.length; i++) {
|
|
79
|
-
const character = csv[i];
|
|
80
|
-
const nextCharacter = i + 1 < csv.length ? csv[i + 1] : "";
|
|
81
|
-
if (character === "\"") if (inQuotes && nextCharacter === "\"") {
|
|
82
|
-
currentField += "\"";
|
|
83
|
-
i++;
|
|
84
|
-
} else inQuotes = !inQuotes;
|
|
85
|
-
else if (character === delimiter && !inQuotes) appendField();
|
|
86
|
-
else if ((character === "\n" || character === "\r" && nextCharacter === "\n") && !inQuotes) {
|
|
87
|
-
if (character === "\r") i++;
|
|
88
|
-
appendRow();
|
|
89
|
-
currentRowNumber++;
|
|
90
|
-
} else currentField += character;
|
|
91
|
-
}
|
|
92
|
-
if (currentField || currentRow.length > 0) appendRow();
|
|
93
|
-
if (inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${currentRowNumber}`);
|
|
94
|
-
if (rows.length <= 1) return [];
|
|
95
|
-
const [headerRow] = rows;
|
|
96
|
-
if (!headerRow) return [];
|
|
97
|
-
const headers = trim ? headerRow.map((h) => h.trim()) : headerRow;
|
|
98
|
-
if (headers.filter((h) => h.length === 0).length > 0) {
|
|
99
|
-
const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
|
|
100
|
-
throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
|
|
101
|
-
}
|
|
102
|
-
const headerSet = /* @__PURE__ */ new Set();
|
|
103
|
-
const duplicateHeaderNames = /* @__PURE__ */ new Set();
|
|
104
|
-
for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
|
|
105
|
-
else headerSet.add(header);
|
|
106
|
-
if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
|
|
107
|
-
const isFieldPopulated = trim ? (field) => field.trim().length > 0 : (field) => field.length > 0;
|
|
108
|
-
return rows.slice(1).filter((row) => row.length > 1 || row.some(isFieldPopulated)).map((fieldValues, rowIndex) => {
|
|
109
|
-
if (fieldValues.length > headers.length) {
|
|
110
|
-
const containsNonEmptyOverflow = fieldValues.slice(headers.length).some(isFieldPopulated);
|
|
111
|
-
if (strict && containsNonEmptyOverflow) {
|
|
112
|
-
const expectedCount = headers.length;
|
|
113
|
-
const actualCount = fieldValues.length;
|
|
114
|
-
const excessCount = actualCount - expectedCount;
|
|
115
|
-
throw new SyntaxError(`CSV row ${rowIndex + 2} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return Object.fromEntries(headers.map((header, columnIndex) => {
|
|
119
|
-
const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
|
|
120
|
-
return [header, trim ? untrimmedValue.trim() : untrimmedValue];
|
|
121
|
-
}));
|
|
228
|
+
const parser = new CSVParserCore(options, (row) => {
|
|
229
|
+
rows.push(row);
|
|
122
230
|
});
|
|
231
|
+
parser.push(csv);
|
|
232
|
+
parser.finish();
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parses CSV data from an async iterable or iterable of string chunks.
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* This function yields CSV rows as they are parsed. Chunks do not need to
|
|
240
|
+
* align with row boundaries; the parser handles quotes and newlines correctly
|
|
241
|
+
* across chunk boundaries.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const chunks = ['name,age\nJo', 'hn,30\nJane,25']
|
|
245
|
+
*
|
|
246
|
+
* for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
|
|
247
|
+
* console.log(row)
|
|
248
|
+
* }
|
|
249
|
+
*/
|
|
250
|
+
async function* parseCSVStream(chunks, options = {}) {
|
|
251
|
+
const queue = [];
|
|
252
|
+
const parser = new CSVParserCore(options, (row) => {
|
|
253
|
+
queue.push(row);
|
|
254
|
+
});
|
|
255
|
+
for await (const chunk of chunks) {
|
|
256
|
+
parser.push(chunk);
|
|
257
|
+
while (queue.length > 0) yield queue.shift();
|
|
258
|
+
}
|
|
259
|
+
parser.finish();
|
|
260
|
+
while (queue.length > 0) yield queue.shift();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parses CSV data from an async iterable or iterable of lines.
|
|
264
|
+
*
|
|
265
|
+
* @remarks
|
|
266
|
+
* This is a convenience wrapper around `parseCSVStream` that treats each line
|
|
267
|
+
* as a chunk. Note that lines do not necessarily correspond to CSV rows due to
|
|
268
|
+
* quoted fields containing newlines. The parser handles this correctly.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* const lines = ['name,age', 'John,30', 'Jane,25']
|
|
272
|
+
*
|
|
273
|
+
* for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
|
|
274
|
+
* console.log(row)
|
|
275
|
+
* }
|
|
276
|
+
*/
|
|
277
|
+
async function* parseCSVFromLines(lines, options) {
|
|
278
|
+
yield* parseCSVStream(lines, options);
|
|
123
279
|
}
|
|
124
280
|
/**
|
|
125
281
|
* Infers column names from data by collecting the union of keys
|
|
@@ -137,4 +293,4 @@ function inferColumns(rows) {
|
|
|
137
293
|
}
|
|
138
294
|
|
|
139
295
|
//#endregion
|
|
140
|
-
export { createCSV, escapeCSVValue, parseCSV };
|
|
296
|
+
export { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MaybeArray, toArray } from "./array.mjs";
|
|
2
|
-
import { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
|
|
2
|
+
import { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
|
|
3
3
|
import { DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
|
|
4
4
|
import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.mjs";
|
|
5
5
|
import { cloneJSON, tryParseJSON } from "./json.mjs";
|
|
@@ -9,4 +9,4 @@ import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSla
|
|
|
9
9
|
import { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { generateRandomId, template } from "./string.mjs";
|
|
11
11
|
import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
|
|
12
|
-
export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
12
|
+
export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toArray } from "./array.mjs";
|
|
2
|
-
import { createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
|
|
2
|
+
import { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
|
|
3
3
|
import { createDefu, defu } from "./defu.mjs";
|
|
4
4
|
import { createEmitter } from "./emitter.mjs";
|
|
5
5
|
import { cloneJSON, tryParseJSON } from "./json.mjs";
|
|
@@ -9,4 +9,4 @@ import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTraili
|
|
|
9
9
|
import { Err, Ok, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { generateRandomId, template } from "./string.mjs";
|
|
11
11
|
|
|
12
|
-
export { Err, Ok, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
12
|
+
export { Err, Ok, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/json.mjs
CHANGED
|
@@ -21,11 +21,11 @@ function tryParseJSON(value) {
|
|
|
21
21
|
*/
|
|
22
22
|
function cloneJSON(value) {
|
|
23
23
|
if (typeof value !== "object" || value === null) return value;
|
|
24
|
-
if (Array.isArray(value)) return value.map((
|
|
24
|
+
if (Array.isArray(value)) return value.map((element) => typeof element !== "object" || element === null ? element : cloneJSON(element));
|
|
25
25
|
const result = {};
|
|
26
|
-
for (const
|
|
27
|
-
const
|
|
28
|
-
result[
|
|
26
|
+
for (const key in value) {
|
|
27
|
+
const propertyValue = value[key];
|
|
28
|
+
result[key] = typeof propertyValue !== "object" || propertyValue === null ? propertyValue : cloneJSON(propertyValue);
|
|
29
29
|
}
|
|
30
30
|
return result;
|
|
31
31
|
}
|
package/package.json
CHANGED