jtcsv 1.1.0 → 2.1.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/README.md +275 -317
- package/bin/jtcsv.js +171 -89
- package/cli-tui.js +0 -0
- package/csv-to-json.js +135 -22
- package/dist/jtcsv.cjs.js +1619 -0
- package/dist/jtcsv.cjs.js.map +1 -0
- package/dist/jtcsv.esm.js +1599 -0
- package/dist/jtcsv.esm.js.map +1 -0
- package/dist/jtcsv.umd.js +1625 -0
- package/dist/jtcsv.umd.js.map +1 -0
- package/examples/cli-tool.js +186 -0
- package/examples/express-api.js +167 -0
- package/examples/large-dataset-example.js +185 -0
- package/examples/plugin-excel-exporter.js +407 -0
- package/examples/simple-usage.js +280 -0
- package/examples/streaming-example.js +419 -0
- package/index.d.ts +22 -3
- package/index.js +1 -0
- package/json-save.js +1 -1
- package/json-to-csv.js +16 -6
- package/package.json +130 -16
- package/plugins/README.md +373 -0
- package/plugins/express-middleware/README.md +306 -0
- package/plugins/express-middleware/example.js +136 -0
- package/plugins/express-middleware/index.d.ts +114 -0
- package/plugins/express-middleware/index.js +360 -0
- package/plugins/express-middleware/package.json +52 -0
- package/plugins/fastify-plugin/index.js +406 -0
- package/plugins/fastify-plugin/package.json +55 -0
- package/plugins/nextjs-api/README.md +452 -0
- package/plugins/nextjs-api/examples/ConverterComponent.jsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +69 -0
- package/plugins/nextjs-api/index.js +388 -0
- package/plugins/nextjs-api/package.json +63 -0
- package/plugins/nextjs-api/route.js +372 -0
- package/src/browser/browser-functions.js +189 -0
- package/src/browser/csv-to-json-browser.js +442 -0
- package/src/browser/errors-browser.js +194 -0
- package/src/browser/index.js +79 -0
- package/src/browser/json-to-csv-browser.js +309 -0
- package/src/browser/workers/csv-parser.worker.js +359 -0
- package/src/browser/workers/worker-pool.js +467 -0
- package/src/core/plugin-system.js +472 -0
- package/src/engines/fast-path-engine-new.js +338 -0
- package/src/engines/fast-path-engine.js +347 -0
- package/src/formats/ndjson-parser.js +419 -0
- package/src/index-with-plugins.js +349 -0
- package/stream-csv-to-json.js +1 -1
- package/stream-json-to-csv.js +1 -1
package/bin/jtcsv.js
CHANGED
|
@@ -41,8 +41,8 @@ ${color('USAGE:', 'bright')}
|
|
|
41
41
|
jtcsv [command] [options] [file...]
|
|
42
42
|
|
|
43
43
|
${color('COMMANDS:', 'bright')}
|
|
44
|
-
${color('
|
|
45
|
-
${color('
|
|
44
|
+
${color('json-to-csv', 'green')} Convert JSON to CSV (alias: json2csv)
|
|
45
|
+
${color('csv-to-json', 'green')} Convert CSV to JSON (alias: csv2json)
|
|
46
46
|
${color('stream', 'yellow')} Streaming conversion for large files
|
|
47
47
|
${color('batch', 'yellow')} Batch process multiple files
|
|
48
48
|
${color('tui', 'magenta')} Launch Terminal User Interface (requires blessed)
|
|
@@ -51,25 +51,31 @@ ${color('COMMANDS:', 'bright')}
|
|
|
51
51
|
|
|
52
52
|
${color('EXAMPLES:', 'bright')}
|
|
53
53
|
${color('Convert JSON file to CSV:', 'dim')}
|
|
54
|
-
jtcsv
|
|
54
|
+
jtcsv json-to-csv input.json output.csv --delimiter=,
|
|
55
55
|
|
|
56
56
|
${color('Convert CSV file to JSON:', 'dim')}
|
|
57
|
-
jtcsv
|
|
57
|
+
jtcsv csv-to-json input.csv output.json --parse-numbers --auto-detect
|
|
58
58
|
|
|
59
59
|
${color('Stream large JSON file to CSV:', 'dim')}
|
|
60
|
-
jtcsv stream
|
|
60
|
+
jtcsv stream json-to-csv large.json output.csv --max-records=1000000
|
|
61
61
|
|
|
62
62
|
${color('Launch TUI interface:', 'dim')}
|
|
63
63
|
jtcsv tui
|
|
64
64
|
|
|
65
65
|
${color('OPTIONS:', 'bright')}
|
|
66
66
|
${color('--delimiter=', 'cyan')}CHAR CSV delimiter (default: ;)
|
|
67
|
+
${color('--auto-detect', 'cyan')} Auto-detect delimiter (default: true)
|
|
68
|
+
${color('--candidates=', 'cyan')}LIST Delimiter candidates (default: ;,\t|)
|
|
67
69
|
${color('--no-headers', 'cyan')} Exclude headers from CSV output
|
|
68
70
|
${color('--parse-numbers', 'cyan')} Parse numeric values in CSV
|
|
69
71
|
${color('--parse-booleans', 'cyan')} Parse boolean values in CSV
|
|
72
|
+
${color('--no-trim', 'cyan')} Don't trim whitespace from CSV values
|
|
73
|
+
${color('--rename=', 'cyan')}JSON Rename columns (JSON map)
|
|
74
|
+
${color('--template=', 'cyan')}JSON Column order template (JSON object)
|
|
70
75
|
${color('--no-injection-protection', 'cyan')} Disable CSV injection protection
|
|
71
|
-
${color('--
|
|
72
|
-
${color('--max-
|
|
76
|
+
${color('--no-rfc4180', 'cyan')} Disable RFC 4180 compliance
|
|
77
|
+
${color('--max-records=', 'cyan')}N Maximum records to process (optional, no limit by default)
|
|
78
|
+
${color('--max-rows=', 'cyan')}N Maximum rows to process (optional, no limit by default)
|
|
73
79
|
${color('--pretty', 'cyan')} Pretty print JSON output
|
|
74
80
|
${color('--silent', 'cyan')} Suppress all output except errors
|
|
75
81
|
${color('--verbose', 'cyan')} Show detailed progress information
|
|
@@ -109,8 +115,19 @@ async function convertJsonToCsv(inputFile, outputFile, options) {
|
|
|
109
115
|
|
|
110
116
|
console.log(color(`Converting ${jsonData.length} records...`, 'dim'));
|
|
111
117
|
|
|
118
|
+
// Prepare options for jtcsv
|
|
119
|
+
const jtcsvOptions = {
|
|
120
|
+
delimiter: options.delimiter,
|
|
121
|
+
includeHeaders: options.includeHeaders,
|
|
122
|
+
renameMap: options.renameMap,
|
|
123
|
+
template: options.template,
|
|
124
|
+
maxRecords: options.maxRecords,
|
|
125
|
+
preventCsvInjection: options.preventCsvInjection,
|
|
126
|
+
rfc4180Compliant: options.rfc4180Compliant
|
|
127
|
+
};
|
|
128
|
+
|
|
112
129
|
// Convert to CSV
|
|
113
|
-
const csvData = jtcsv.jsonToCsv(jsonData,
|
|
130
|
+
const csvData = jtcsv.jsonToCsv(jsonData, jtcsvOptions);
|
|
114
131
|
|
|
115
132
|
// Write output file
|
|
116
133
|
await fs.promises.writeFile(outputFile, csvData, 'utf8');
|
|
@@ -129,10 +146,23 @@ async function convertCsvToJson(inputFile, outputFile, options) {
|
|
|
129
146
|
const startTime = Date.now();
|
|
130
147
|
|
|
131
148
|
try {
|
|
132
|
-
console.log(color(
|
|
149
|
+
console.log(color('Reading CSV file...', 'dim'));
|
|
150
|
+
|
|
151
|
+
// Prepare options for jtcsv
|
|
152
|
+
const jtcsvOptions = {
|
|
153
|
+
delimiter: options.delimiter,
|
|
154
|
+
autoDetect: options.autoDetect,
|
|
155
|
+
candidates: options.candidates,
|
|
156
|
+
hasHeaders: options.hasHeaders,
|
|
157
|
+
renameMap: options.renameMap,
|
|
158
|
+
trim: options.trim,
|
|
159
|
+
parseNumbers: options.parseNumbers,
|
|
160
|
+
parseBooleans: options.parseBooleans,
|
|
161
|
+
maxRows: options.maxRows
|
|
162
|
+
};
|
|
133
163
|
|
|
134
164
|
// Read and convert CSV
|
|
135
|
-
const jsonData = await jtcsv.readCsvAsJson(inputFile,
|
|
165
|
+
const jsonData = await jtcsv.readCsvAsJson(inputFile, jtcsvOptions);
|
|
136
166
|
|
|
137
167
|
// Format JSON
|
|
138
168
|
const jsonOutput = options.pretty
|
|
@@ -156,7 +186,7 @@ async function streamJsonToCsv(inputFile, outputFile, options) {
|
|
|
156
186
|
const startTime = Date.now();
|
|
157
187
|
|
|
158
188
|
try {
|
|
159
|
-
console.log(color(
|
|
189
|
+
console.log(color('Streaming conversion started...', 'dim'));
|
|
160
190
|
|
|
161
191
|
// Create streams
|
|
162
192
|
const readStream = fs.createReadStream(inputFile, 'utf8');
|
|
@@ -235,12 +265,19 @@ async function launchTUI() {
|
|
|
235
265
|
function parseOptions(args) {
|
|
236
266
|
const options = {
|
|
237
267
|
delimiter: ';',
|
|
268
|
+
autoDetect: true,
|
|
269
|
+
candidates: [';', ',', '\t', '|'],
|
|
270
|
+
hasHeaders: true,
|
|
238
271
|
includeHeaders: true,
|
|
272
|
+
renameMap: undefined,
|
|
273
|
+
template: undefined,
|
|
274
|
+
trim: true,
|
|
239
275
|
parseNumbers: false,
|
|
240
276
|
parseBooleans: false,
|
|
241
277
|
preventCsvInjection: true,
|
|
242
|
-
|
|
243
|
-
|
|
278
|
+
rfc4180Compliant: true,
|
|
279
|
+
maxRecords: undefined,
|
|
280
|
+
maxRows: undefined,
|
|
244
281
|
pretty: false,
|
|
245
282
|
silent: false,
|
|
246
283
|
verbose: false
|
|
@@ -255,36 +292,72 @@ function parseOptions(args) {
|
|
|
255
292
|
const [key, value] = arg.slice(2).split('=');
|
|
256
293
|
|
|
257
294
|
switch (key) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
case 'delimiter':
|
|
296
|
+
options.delimiter = value || ',';
|
|
297
|
+
options.autoDetect = false; // Disable auto-detect if delimiter is specified
|
|
298
|
+
break;
|
|
299
|
+
case 'auto-detect':
|
|
300
|
+
options.autoDetect = value !== 'false';
|
|
301
|
+
break;
|
|
302
|
+
case 'candidates':
|
|
303
|
+
options.candidates = value ? value.split(',') : [';', ',', '\t', '|'];
|
|
304
|
+
break;
|
|
305
|
+
case 'no-headers':
|
|
306
|
+
options.includeHeaders = false;
|
|
307
|
+
options.hasHeaders = false;
|
|
308
|
+
break;
|
|
309
|
+
case 'parse-numbers':
|
|
310
|
+
options.parseNumbers = true;
|
|
311
|
+
break;
|
|
312
|
+
case 'parse-booleans':
|
|
313
|
+
options.parseBooleans = true;
|
|
314
|
+
break;
|
|
315
|
+
case 'no-trim':
|
|
316
|
+
options.trim = false;
|
|
317
|
+
break;
|
|
318
|
+
case 'rename':
|
|
319
|
+
try {
|
|
320
|
+
// Handle both quoted and unquoted JSON
|
|
321
|
+
const jsonStr = value || '{}';
|
|
322
|
+
// Remove surrounding single quotes if present
|
|
323
|
+
const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
|
|
324
|
+
options.renameMap = JSON.parse(cleanStr);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
throw new Error(`Invalid JSON in --rename option: ${e.message}. Value: ${value}`);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case 'template':
|
|
330
|
+
try {
|
|
331
|
+
// Handle both quoted and unquoted JSON
|
|
332
|
+
const jsonStr = value || '{}';
|
|
333
|
+
// Remove surrounding single quotes if present
|
|
334
|
+
const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
|
|
335
|
+
options.template = JSON.parse(cleanStr);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
throw new Error(`Invalid JSON in --template option: ${e.message}. Value: ${value}`);
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
case 'no-injection-protection':
|
|
341
|
+
options.preventCsvInjection = false;
|
|
342
|
+
break;
|
|
343
|
+
case 'no-rfc4180':
|
|
344
|
+
options.rfc4180Compliant = false;
|
|
345
|
+
break;
|
|
346
|
+
case 'max-records':
|
|
347
|
+
options.maxRecords = parseInt(value, 10);
|
|
348
|
+
break;
|
|
349
|
+
case 'max-rows':
|
|
350
|
+
options.maxRows = parseInt(value, 10);
|
|
351
|
+
break;
|
|
352
|
+
case 'pretty':
|
|
353
|
+
options.pretty = true;
|
|
354
|
+
break;
|
|
355
|
+
case 'silent':
|
|
356
|
+
options.silent = true;
|
|
357
|
+
break;
|
|
358
|
+
case 'verbose':
|
|
359
|
+
options.verbose = true;
|
|
360
|
+
break;
|
|
288
361
|
}
|
|
289
362
|
} else if (!arg.startsWith('-')) {
|
|
290
363
|
files.push(arg);
|
|
@@ -312,57 +385,59 @@ async function main() {
|
|
|
312
385
|
}
|
|
313
386
|
|
|
314
387
|
switch (command) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
388
|
+
case 'json-to-csv':
|
|
389
|
+
case 'json2csv': // Backward compatibility
|
|
390
|
+
if (files.length < 2) {
|
|
391
|
+
console.error(color('Error: Input and output files required', 'red'));
|
|
392
|
+
console.log(color('Usage: jtcsv json-to-csv input.json output.csv', 'cyan'));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
await convertJsonToCsv(files[0], files[1], options);
|
|
396
|
+
break;
|
|
323
397
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
398
|
+
case 'csv-to-json':
|
|
399
|
+
case 'csv2json': // Backward compatibility
|
|
400
|
+
if (files.length < 2) {
|
|
401
|
+
console.error(color('Error: Input and output files required', 'red'));
|
|
402
|
+
console.log(color('Usage: jtcsv csv-to-json input.csv output.json', 'cyan'));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
await convertCsvToJson(files[0], files[1], options);
|
|
406
|
+
break;
|
|
332
407
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
408
|
+
case 'stream':
|
|
409
|
+
if (args.length < 2) {
|
|
410
|
+
console.error(color('Error: Streaming mode requires subcommand', 'red'));
|
|
411
|
+
console.log(color('Usage: jtcsv stream [json2csv|csv2json] input output', 'cyan'));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const streamCommand = args[1].toLowerCase();
|
|
415
|
+
if (streamCommand === 'json2csv' && files.length >= 2) {
|
|
416
|
+
await streamJsonToCsv(files[0], files[1], options);
|
|
417
|
+
} else {
|
|
418
|
+
console.error(color('Error: Invalid streaming command', 'red'));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
347
422
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
423
|
+
case 'tui':
|
|
424
|
+
await launchTUI();
|
|
425
|
+
break;
|
|
351
426
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
427
|
+
case 'help':
|
|
428
|
+
showHelp();
|
|
429
|
+
break;
|
|
355
430
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
431
|
+
case 'version':
|
|
432
|
+
case '-v':
|
|
433
|
+
case '--version':
|
|
434
|
+
showVersion();
|
|
435
|
+
break;
|
|
361
436
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
437
|
+
default:
|
|
438
|
+
console.error(color(`Error: Unknown command '${command}'`, 'red'));
|
|
439
|
+
console.log(color('Use jtcsv help for available commands', 'cyan'));
|
|
440
|
+
process.exit(1);
|
|
366
441
|
}
|
|
367
442
|
}
|
|
368
443
|
|
|
@@ -391,4 +466,11 @@ if (require.main === module) {
|
|
|
391
466
|
});
|
|
392
467
|
}
|
|
393
468
|
|
|
394
|
-
module.exports = { main };
|
|
469
|
+
module.exports = { main };
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
|
package/cli-tui.js
CHANGED
|
Binary file
|
package/csv-to-json.js
CHANGED
|
@@ -41,8 +41,18 @@ function validateCsvInput(csv, options) {
|
|
|
41
41
|
throw new ConfigurationError('Delimiter must be a single character');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Validate autoDetect
|
|
45
|
+
if (options?.autoDetect !== undefined && typeof options.autoDetect !== 'boolean') {
|
|
46
|
+
throw new ConfigurationError('autoDetect must be a boolean');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate candidates
|
|
50
|
+
if (options?.candidates && !Array.isArray(options.candidates)) {
|
|
51
|
+
throw new ConfigurationError('candidates must be an array');
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
// Validate maxRows
|
|
45
|
-
if (options?.maxRows && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
|
|
55
|
+
if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
|
|
46
56
|
throw new ConfigurationError('maxRows must be a positive number');
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -50,7 +60,7 @@ function validateCsvInput(csv, options) {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
/**
|
|
53
|
-
* Parses a single CSV line with proper escaping
|
|
63
|
+
но * Parses a single CSV line with proper escaping
|
|
54
64
|
* @private
|
|
55
65
|
*/
|
|
56
66
|
function parseCsvLine(line, lineNumber, delimiter) {
|
|
@@ -69,7 +79,17 @@ function parseCsvLine(line, lineNumber, delimiter) {
|
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
if (char === '\\') {
|
|
72
|
-
|
|
82
|
+
if (i + 1 === line.length) {
|
|
83
|
+
// Backslash at end of line - treat as literal
|
|
84
|
+
currentField += char;
|
|
85
|
+
} else if (line[i + 1] === '\\') {
|
|
86
|
+
// Double backslash - add one backslash to field and skip next
|
|
87
|
+
currentField += char;
|
|
88
|
+
i++; // Skip next backslash
|
|
89
|
+
} else {
|
|
90
|
+
// Escape next character
|
|
91
|
+
escapeNext = true;
|
|
92
|
+
}
|
|
73
93
|
continue;
|
|
74
94
|
}
|
|
75
95
|
|
|
@@ -87,6 +107,22 @@ function parseCsvLine(line, lineNumber, delimiter) {
|
|
|
87
107
|
// Escaped quote inside quotes ("" -> ")
|
|
88
108
|
currentField += '"';
|
|
89
109
|
i++; // Skip next quote
|
|
110
|
+
// Check if this is the end of the quoted field
|
|
111
|
+
// Look ahead to see if next char is delimiter or end of line
|
|
112
|
+
let isEndOfField = false;
|
|
113
|
+
let j = i + 1;
|
|
114
|
+
// Skip whitespace
|
|
115
|
+
while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
if (j === line.length || line[j] === delimiter) {
|
|
119
|
+
isEndOfField = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isEndOfField) {
|
|
123
|
+
// This is the closing quote
|
|
124
|
+
insideQuotes = false;
|
|
125
|
+
}
|
|
90
126
|
}
|
|
91
127
|
} else {
|
|
92
128
|
// Check if this is really the end of the quoted field
|
|
@@ -126,6 +162,13 @@ function parseCsvLine(line, lineNumber, delimiter) {
|
|
|
126
162
|
currentField += char;
|
|
127
163
|
}
|
|
128
164
|
|
|
165
|
+
// Handle case where escapeNext is still true at end of line
|
|
166
|
+
if (escapeNext) {
|
|
167
|
+
// This happens when line ends with backslash
|
|
168
|
+
// Add the backslash as literal character
|
|
169
|
+
currentField += '\\';
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
// Add last field
|
|
130
173
|
fields.push(currentField);
|
|
131
174
|
|
|
@@ -171,11 +214,11 @@ function parseCsvValue(value, options) {
|
|
|
171
214
|
// Parse booleans
|
|
172
215
|
if (parseBooleans) {
|
|
173
216
|
const lowerValue = result.toLowerCase();
|
|
174
|
-
if (lowerValue === 'true') {
|
|
175
|
-
return true;
|
|
217
|
+
if (lowerValue === 'true') {
|
|
218
|
+
return true;
|
|
176
219
|
}
|
|
177
|
-
if (lowerValue === 'false') {
|
|
178
|
-
return false;
|
|
220
|
+
if (lowerValue === 'false') {
|
|
221
|
+
return false;
|
|
179
222
|
}
|
|
180
223
|
}
|
|
181
224
|
|
|
@@ -187,18 +230,66 @@ function parseCsvValue(value, options) {
|
|
|
187
230
|
return result;
|
|
188
231
|
}
|
|
189
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Auto-detect CSV delimiter from content
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
function autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|']) {
|
|
238
|
+
if (!csv || typeof csv !== 'string') {
|
|
239
|
+
return ';'; // default
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lines = csv.split('\n').filter(line => line.trim().length > 0);
|
|
243
|
+
|
|
244
|
+
if (lines.length === 0) {
|
|
245
|
+
return ';'; // default
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Use first non-empty line for detection
|
|
249
|
+
const firstLine = lines[0];
|
|
250
|
+
|
|
251
|
+
const counts = {};
|
|
252
|
+
candidates.forEach(delim => {
|
|
253
|
+
// Escape special regex characters
|
|
254
|
+
const escapedDelim = delim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
255
|
+
const regex = new RegExp(escapedDelim, 'g');
|
|
256
|
+
const matches = firstLine.match(regex);
|
|
257
|
+
counts[delim] = matches ? matches.length : 0;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Find delimiter with maximum count
|
|
261
|
+
let maxCount = -1;
|
|
262
|
+
let detectedDelimiter = ';'; // default
|
|
263
|
+
|
|
264
|
+
for (const [delim, count] of Object.entries(counts)) {
|
|
265
|
+
if (count > maxCount) {
|
|
266
|
+
maxCount = count;
|
|
267
|
+
detectedDelimiter = delim;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If no delimiter found or tie, return default
|
|
272
|
+
if (maxCount === 0) {
|
|
273
|
+
return ';'; // default
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return detectedDelimiter;
|
|
277
|
+
}
|
|
278
|
+
|
|
190
279
|
/**
|
|
191
280
|
* Converts CSV string to JSON array
|
|
192
281
|
*
|
|
193
|
-
|
|
282
|
+
* @param {string} csv - CSV string to convert
|
|
194
283
|
* @param {Object} [options] - Configuration options
|
|
195
|
-
* @param {string} [options.delimiter
|
|
284
|
+
* @param {string} [options.delimiter] - CSV delimiter character (default: auto-detected)
|
|
285
|
+
* @param {boolean} [options.autoDetect=true] - Auto-detect delimiter if not specified
|
|
286
|
+
* @param {Array} [options.candidates=[';', ',', '\t', '|']] - Candidate delimiters for auto-detection
|
|
196
287
|
* @param {boolean} [options.hasHeaders=true] - Whether CSV has headers row
|
|
197
288
|
* @param {Object} [options.renameMap={}] - Map for renaming column headers (newKey: oldKey)
|
|
198
289
|
* @param {boolean} [options.trim=true] - Trim whitespace from values
|
|
199
290
|
* @param {boolean} [options.parseNumbers=false] - Parse numeric values
|
|
200
291
|
* @param {boolean} [options.parseBooleans=false] - Parse boolean values
|
|
201
|
-
* @param {number} [options.maxRows
|
|
292
|
+
* @param {number} [options.maxRows] - Maximum number of rows to process (optional, no limit by default)
|
|
202
293
|
* @returns {Array<Object>} JSON array
|
|
203
294
|
*
|
|
204
295
|
* @example
|
|
@@ -218,15 +309,24 @@ function csvToJson(csv, options = {}) {
|
|
|
218
309
|
const opts = options && typeof options === 'object' ? options : {};
|
|
219
310
|
|
|
220
311
|
const {
|
|
221
|
-
delimiter
|
|
312
|
+
delimiter,
|
|
313
|
+
autoDetect = true,
|
|
314
|
+
candidates = [';', ',', '\t', '|'],
|
|
222
315
|
hasHeaders = true,
|
|
223
316
|
renameMap = {},
|
|
224
317
|
trim = true,
|
|
225
318
|
parseNumbers = false,
|
|
226
319
|
parseBooleans = false,
|
|
227
|
-
maxRows
|
|
320
|
+
maxRows
|
|
228
321
|
} = opts;
|
|
229
322
|
|
|
323
|
+
// Determine delimiter
|
|
324
|
+
let finalDelimiter = delimiter;
|
|
325
|
+
if (!finalDelimiter && autoDetect) {
|
|
326
|
+
finalDelimiter = autoDetectDelimiter(csv, candidates);
|
|
327
|
+
}
|
|
328
|
+
finalDelimiter = finalDelimiter || ';'; // fallback
|
|
329
|
+
|
|
230
330
|
// Handle empty CSV
|
|
231
331
|
if (csv.trim() === '') {
|
|
232
332
|
return [];
|
|
@@ -274,16 +374,28 @@ function csvToJson(csv, options = {}) {
|
|
|
274
374
|
}
|
|
275
375
|
|
|
276
376
|
// Check for unclosed quotes
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
377
|
+
// Note: This check is moved to parseCsvLine which has better context
|
|
378
|
+
// for handling escaped quotes like ""
|
|
379
|
+
// if (insideQuotes) {
|
|
380
|
+
// throw new ParsingError('Unclosed quotes in CSV', lines.length);
|
|
381
|
+
// }
|
|
280
382
|
|
|
281
383
|
if (lines.length === 0) {
|
|
282
384
|
return [];
|
|
283
385
|
}
|
|
284
386
|
|
|
285
|
-
//
|
|
286
|
-
if (lines.length > maxRows) {
|
|
387
|
+
// Show warning for large datasets (optional limit)
|
|
388
|
+
if (lines.length > 1000000 && !maxRows && process.env.NODE_ENV !== 'test') {
|
|
389
|
+
console.warn(
|
|
390
|
+
'⚠️ Warning: Processing >1M records in memory may be slow.\n' +
|
|
391
|
+
'💡 Consider using createCsvToJsonStream() for better performance with large files.\n' +
|
|
392
|
+
'📊 Current size: ' + lines.length.toLocaleString() + ' rows\n' +
|
|
393
|
+
'🔧 Tip: Use { maxRows: N } option to set a custom limit if needed.'
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Apply optional row limit if specified
|
|
398
|
+
if (maxRows && lines.length > maxRows) {
|
|
287
399
|
throw new LimitError(
|
|
288
400
|
`CSV size exceeds maximum limit of ${maxRows} rows`,
|
|
289
401
|
maxRows,
|
|
@@ -297,7 +409,7 @@ function csvToJson(csv, options = {}) {
|
|
|
297
409
|
// Parse headers if present
|
|
298
410
|
if (hasHeaders && lines.length > 0) {
|
|
299
411
|
try {
|
|
300
|
-
headers = parseCsvLine(lines[0], 1,
|
|
412
|
+
headers = parseCsvLine(lines[0], 1, finalDelimiter).map(header => {
|
|
301
413
|
const trimmed = trim ? header.trim() : header;
|
|
302
414
|
// Apply rename map
|
|
303
415
|
return renameMap[trimmed] || trimmed;
|
|
@@ -312,7 +424,7 @@ function csvToJson(csv, options = {}) {
|
|
|
312
424
|
} else {
|
|
313
425
|
// Generate numeric headers from first line
|
|
314
426
|
try {
|
|
315
|
-
const firstLineFields = parseCsvLine(lines[0], 1,
|
|
427
|
+
const firstLineFields = parseCsvLine(lines[0], 1, finalDelimiter);
|
|
316
428
|
headers = firstLineFields.map((_, index) => `column${index + 1}`);
|
|
317
429
|
} catch (error) {
|
|
318
430
|
if (error instanceof ParsingError) {
|
|
@@ -334,7 +446,7 @@ function csvToJson(csv, options = {}) {
|
|
|
334
446
|
}
|
|
335
447
|
|
|
336
448
|
try {
|
|
337
|
-
const fields = parseCsvLine(line, i + 1,
|
|
449
|
+
const fields = parseCsvLine(line, i + 1, finalDelimiter);
|
|
338
450
|
|
|
339
451
|
// Handle mismatched field count
|
|
340
452
|
const row = {};
|
|
@@ -399,7 +511,7 @@ function validateCsvFilePath(filePath) {
|
|
|
399
511
|
* @returns {Promise<Array<Object>>} Promise that resolves to JSON array
|
|
400
512
|
*
|
|
401
513
|
* @example
|
|
402
|
-
*
|
|
514
|
+
* const { readCsvAsJson } = require('./csv-to-json');
|
|
403
515
|
*
|
|
404
516
|
* const json = await readCsvAsJson('./data.csv', {
|
|
405
517
|
* delimiter: ',',
|
|
@@ -483,7 +595,8 @@ function readCsvAsJsonSync(filePath, options = {}) {
|
|
|
483
595
|
module.exports = {
|
|
484
596
|
csvToJson,
|
|
485
597
|
readCsvAsJson,
|
|
486
|
-
readCsvAsJsonSync
|
|
598
|
+
readCsvAsJsonSync,
|
|
599
|
+
autoDetectDelimiter
|
|
487
600
|
};
|
|
488
601
|
|
|
489
602
|
// For ES6 module compatibility
|