csv-stream-lite 1.0.2 → 1.0.4
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/parser.d.ts +23 -1
- package/dist/parser.js +107 -15
- package/package.json +1 -1
package/dist/parser.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface CsvEntityOptions {
|
|
|
9
9
|
separator?: string;
|
|
10
10
|
/** Character used to escape special characters. Defaults to '"' */
|
|
11
11
|
escapeChar?: string;
|
|
12
|
+
/** Character used to quote fields. Defaults to escapeChar value */
|
|
13
|
+
quoteChar?: string;
|
|
14
|
+
/** String used to denote new lines. Defaults to auto-detected '\r', '\n', or '\r\n' */
|
|
15
|
+
newline?: string;
|
|
16
|
+
/** Whether to trim whitespace from fields. Defaults to false. NOTE: this option is not supported when streaming, as trimming requires buffering the entire field. */
|
|
17
|
+
trim?: boolean;
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
20
|
* Abstract base class for CSV entities that supports both synchronous and asynchronous parsing.
|
|
@@ -17,10 +23,13 @@ export interface CsvEntityOptions {
|
|
|
17
23
|
* @typeParam T - The type returned by read operations
|
|
18
24
|
* @typeParam S - The type yielded by stream operations (defaults to T)
|
|
19
25
|
*/
|
|
20
|
-
export declare abstract class CsvEntity<T, S = T>
|
|
26
|
+
export declare abstract class CsvEntity<T, S = T> {
|
|
21
27
|
byteBuffer: ByteBuffer;
|
|
22
28
|
separator: string;
|
|
23
29
|
escapeChar: string;
|
|
30
|
+
quoteChar: string;
|
|
31
|
+
newline?: string;
|
|
32
|
+
trim: boolean;
|
|
24
33
|
consumed: boolean;
|
|
25
34
|
/**
|
|
26
35
|
* Creates a new CSV entity.
|
|
@@ -96,6 +105,19 @@ export declare abstract class CsvEntity<T, S = T> implements Required<CsvEntityO
|
|
|
96
105
|
export declare class CsvCell extends CsvEntity<string> {
|
|
97
106
|
chunkSize: number;
|
|
98
107
|
endOfLineReached: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Checks if the current buffer position starts with a line ending.
|
|
110
|
+
* Supports both default line endings (\r, \n, \r\n) and custom newline strings.
|
|
111
|
+
*
|
|
112
|
+
* @returns true if at the start of a line ending, false otherwise
|
|
113
|
+
*/
|
|
114
|
+
private isAtLineEnd;
|
|
115
|
+
/**
|
|
116
|
+
* Consumes a line ending from the buffer.
|
|
117
|
+
* Handles both default line endings (\r, \n, \r\n) and custom newline strings.
|
|
118
|
+
* Should only be called after isAtLineEnd() returns true.
|
|
119
|
+
*/
|
|
120
|
+
private consumeLineEnd;
|
|
99
121
|
protected parse(): string;
|
|
100
122
|
protected parseAsync(): Promise<string>;
|
|
101
123
|
/**
|
package/dist/parser.js
CHANGED
|
@@ -31,6 +31,9 @@ export class CsvEntity {
|
|
|
31
31
|
byteBuffer;
|
|
32
32
|
separator = ',';
|
|
33
33
|
escapeChar = '"';
|
|
34
|
+
quoteChar = '"';
|
|
35
|
+
newline;
|
|
36
|
+
trim = false;
|
|
34
37
|
consumed = false;
|
|
35
38
|
/**
|
|
36
39
|
* Creates a new CSV entity.
|
|
@@ -49,6 +52,32 @@ export class CsvEntity {
|
|
|
49
52
|
if (options?.escapeChar) {
|
|
50
53
|
this.escapeChar = options.escapeChar;
|
|
51
54
|
}
|
|
55
|
+
if (options?.quoteChar) {
|
|
56
|
+
this.quoteChar = options.quoteChar;
|
|
57
|
+
}
|
|
58
|
+
else if (options?.escapeChar) {
|
|
59
|
+
// Default quoteChar to escapeChar if only escapeChar is specified
|
|
60
|
+
this.quoteChar = options.escapeChar;
|
|
61
|
+
}
|
|
62
|
+
if (options?.trim !== undefined) {
|
|
63
|
+
this.trim = options.trim;
|
|
64
|
+
}
|
|
65
|
+
if (options?.newline !== undefined) {
|
|
66
|
+
const newline = options.newline;
|
|
67
|
+
if (newline === '') {
|
|
68
|
+
throw new Error('Invalid CSV newline: newline option must be a non-empty string.');
|
|
69
|
+
}
|
|
70
|
+
if (newline.includes(this.separator)) {
|
|
71
|
+
throw new Error('Invalid CSV newline: newline option must not contain the field separator character.');
|
|
72
|
+
}
|
|
73
|
+
if (newline.includes(this.escapeChar)) {
|
|
74
|
+
throw new Error('Invalid CSV newline: newline option must not contain the escape character.');
|
|
75
|
+
}
|
|
76
|
+
if (newline.includes(this.quoteChar)) {
|
|
77
|
+
throw new Error('Invalid CSV newline: newline option must not contain the quote character.');
|
|
78
|
+
}
|
|
79
|
+
this.newline = newline;
|
|
80
|
+
}
|
|
52
81
|
}
|
|
53
82
|
set maxBufferSize(size) {
|
|
54
83
|
this.byteBuffer.maxBufferSize = size;
|
|
@@ -162,19 +191,68 @@ export class CsvEntity {
|
|
|
162
191
|
export class CsvCell extends CsvEntity {
|
|
163
192
|
chunkSize = DEFAULT_CHUNK_SIZE;
|
|
164
193
|
endOfLineReached = false;
|
|
194
|
+
/**
|
|
195
|
+
* Checks if the current buffer position starts with a line ending.
|
|
196
|
+
* Supports both default line endings (\r, \n, \r\n) and custom newline strings.
|
|
197
|
+
*
|
|
198
|
+
* @returns true if at the start of a line ending, false otherwise
|
|
199
|
+
*/
|
|
200
|
+
isAtLineEnd() {
|
|
201
|
+
if (this.newline !== undefined) {
|
|
202
|
+
// Check for custom newline string
|
|
203
|
+
for (let i = 0; i < this.newline.length; i++) {
|
|
204
|
+
const expectedByte = this.newline.charCodeAt(i);
|
|
205
|
+
const actualByte = this.byteBuffer.peek(i);
|
|
206
|
+
if (actualByte === null || actualByte !== expectedByte) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Default behavior: check for \r or \n
|
|
214
|
+
return isLineEnd(this.byteBuffer.peek());
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Consumes a line ending from the buffer.
|
|
219
|
+
* Handles both default line endings (\r, \n, \r\n) and custom newline strings.
|
|
220
|
+
* Should only be called after isAtLineEnd() returns true.
|
|
221
|
+
*/
|
|
222
|
+
consumeLineEnd() {
|
|
223
|
+
if (this.newline !== undefined) {
|
|
224
|
+
// Consume custom newline string - verify each byte matches
|
|
225
|
+
for (let i = 0; i < this.newline.length; i++) {
|
|
226
|
+
const expectedByte = this.newline.charCodeAt(i);
|
|
227
|
+
const actualByte = this.byteBuffer.peek();
|
|
228
|
+
if (actualByte === null || actualByte !== expectedByte) {
|
|
229
|
+
throw new Error('Invariant violation: consumeLineEnd called when not at line end');
|
|
230
|
+
}
|
|
231
|
+
this.byteBuffer.next();
|
|
232
|
+
}
|
|
233
|
+
this.endOfLineReached = true;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Default behavior: consume \r and/or \n
|
|
237
|
+
while (isLineEnd(this.byteBuffer.peek())) {
|
|
238
|
+
this.byteBuffer.next();
|
|
239
|
+
this.endOfLineReached = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
165
243
|
parse() {
|
|
166
244
|
let str = '';
|
|
167
245
|
for (const part of this) {
|
|
168
246
|
str += part;
|
|
169
247
|
}
|
|
170
|
-
return str;
|
|
248
|
+
return this.trim ? str.trim() : str;
|
|
171
249
|
}
|
|
172
250
|
async parseAsync() {
|
|
173
251
|
let str = '';
|
|
174
252
|
for await (const part of this) {
|
|
175
253
|
str += part;
|
|
176
254
|
}
|
|
177
|
-
return str;
|
|
255
|
+
return this.trim ? str.trim() : str;
|
|
178
256
|
}
|
|
179
257
|
/**
|
|
180
258
|
* Reads the cell value and transforms it using the provided function.
|
|
@@ -209,22 +287,23 @@ export class CsvCell extends CsvEntity {
|
|
|
209
287
|
*streamImpl() {
|
|
210
288
|
const separator = this.separator.charCodeAt(0);
|
|
211
289
|
const escapeChar = this.escapeChar.charCodeAt(0);
|
|
290
|
+
const quoteChar = this.quoteChar.charCodeAt(0);
|
|
212
291
|
let chunk = [];
|
|
213
292
|
let hadData = false;
|
|
214
|
-
let
|
|
293
|
+
let isQuoted = false;
|
|
215
294
|
const next = this.byteBuffer.peek();
|
|
216
295
|
if (next === null && this.byteBuffer.eof) {
|
|
217
296
|
throw new Error('No more data to read');
|
|
218
297
|
}
|
|
219
|
-
if (next ===
|
|
220
|
-
|
|
221
|
-
this.byteBuffer.expect(
|
|
298
|
+
if (next === quoteChar) {
|
|
299
|
+
isQuoted = true;
|
|
300
|
+
this.byteBuffer.expect(quoteChar); // consume opening quote
|
|
222
301
|
}
|
|
223
302
|
while (this.byteBuffer.peek() !== null) {
|
|
224
303
|
const next = this.byteBuffer.peek();
|
|
225
|
-
if (
|
|
226
|
-
if (next === escapeChar) {
|
|
227
|
-
//
|
|
304
|
+
if (isQuoted) {
|
|
305
|
+
if (next === escapeChar && escapeChar === quoteChar) {
|
|
306
|
+
// Standard CSV: quote char doubles as escape char
|
|
228
307
|
const lookahead = this.byteBuffer.peek(1);
|
|
229
308
|
if (lookahead === escapeChar) {
|
|
230
309
|
// Escaped quote
|
|
@@ -238,9 +317,23 @@ export class CsvCell extends CsvEntity {
|
|
|
238
317
|
break;
|
|
239
318
|
}
|
|
240
319
|
}
|
|
320
|
+
else if (next === quoteChar) {
|
|
321
|
+
// End of quoted cell when using separate quote/escape chars
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
else if (next === escapeChar) {
|
|
325
|
+
// Handle escape character when different from quote char
|
|
326
|
+
const lookahead = this.byteBuffer.peek(1);
|
|
327
|
+
if (lookahead !== null) {
|
|
328
|
+
this.byteBuffer.expect(escapeChar); // consume escape char
|
|
329
|
+
const escapedChar = this.byteBuffer.next(); // consume escaped char
|
|
330
|
+
chunk.push(escapedChar);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
241
334
|
}
|
|
242
335
|
else {
|
|
243
|
-
if (next === separator ||
|
|
336
|
+
if (next === separator || this.isAtLineEnd()) {
|
|
244
337
|
break;
|
|
245
338
|
}
|
|
246
339
|
}
|
|
@@ -252,15 +345,14 @@ export class CsvCell extends CsvEntity {
|
|
|
252
345
|
hadData = true;
|
|
253
346
|
}
|
|
254
347
|
}
|
|
255
|
-
if (
|
|
256
|
-
this.byteBuffer.expect(
|
|
348
|
+
if (isQuoted) {
|
|
349
|
+
this.byteBuffer.expect(quoteChar); // consume closing quote
|
|
257
350
|
}
|
|
258
351
|
if (this.byteBuffer.peek() === separator) {
|
|
259
352
|
this.byteBuffer.expect(separator); // consume separator
|
|
260
353
|
}
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
this.endOfLineReached = true;
|
|
354
|
+
if (this.isAtLineEnd()) {
|
|
355
|
+
this.consumeLineEnd();
|
|
264
356
|
}
|
|
265
357
|
if (!hadData || chunk.length > 0)
|
|
266
358
|
yield bytesToString(new Uint8Array(chunk));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "csv-stream-lite",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A lightweight, memory-efficient, zero-dependency streaming CSV parser and stringifier for JavaScript and TypeScript. It works in both Node.js and browser environments.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|