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 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> implements Required<CsvEntityOptions> {
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 isEscaped = false;
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 === escapeChar) {
220
- isEscaped = true;
221
- this.byteBuffer.expect(escapeChar); // consume opening quote
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 (isEscaped) {
226
- if (next === escapeChar) {
227
- // Possible end of quoted cell
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 || isLineEnd(next)) {
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 (isEscaped) {
256
- this.byteBuffer.expect(escapeChar); // consume closing quote
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
- while (isLineEnd(this.byteBuffer.peek())) {
262
- this.byteBuffer.next(); // consume line ending
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.2",
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",