jtcsv 2.1.0 → 2.1.3
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 +63 -17
- package/bin/jtcsv.js +1013 -117
- package/csv-to-json.js +385 -311
- package/examples/simple-usage.js +2 -3
- package/index.d.ts +288 -5
- package/index.js +23 -0
- package/json-to-csv.js +130 -89
- package/package.json +47 -19
- package/plugins/README.md +146 -2
- package/plugins/hono/README.md +25 -0
- package/plugins/hono/index.d.ts +12 -0
- package/plugins/hono/index.js +36 -0
- package/plugins/hono/package.json +35 -0
- package/plugins/nestjs/README.md +33 -0
- package/plugins/nestjs/index.d.ts +25 -0
- package/plugins/nestjs/index.js +77 -0
- package/plugins/nestjs/package.json +37 -0
- package/plugins/nuxt/README.md +25 -0
- package/plugins/nuxt/index.js +21 -0
- package/plugins/nuxt/package.json +35 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.js +6 -0
- package/plugins/nuxt/runtime/plugin.js +6 -0
- package/plugins/remix/README.md +26 -0
- package/plugins/remix/index.d.ts +16 -0
- package/plugins/remix/index.js +62 -0
- package/plugins/remix/package.json +35 -0
- package/plugins/sveltekit/README.md +28 -0
- package/plugins/sveltekit/index.d.ts +17 -0
- package/plugins/sveltekit/index.js +54 -0
- package/plugins/sveltekit/package.json +33 -0
- package/plugins/trpc/README.md +22 -0
- package/plugins/trpc/index.d.ts +7 -0
- package/plugins/trpc/index.js +32 -0
- package/plugins/trpc/package.json +34 -0
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine.js +829 -340
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +36 -14
- package/cli-tui.js +0 -5
package/examples/simple-usage.js
CHANGED
|
@@ -238,8 +238,8 @@ try {
|
|
|
238
238
|
console.log(' examples/plugin-excel-exporter.js');
|
|
239
239
|
} catch (error) {
|
|
240
240
|
console.log('⚠️ Полная интеграция требует дополнительных зависимостей');
|
|
241
|
-
console.log('Установите exceljs для демонстрации Excel плагина:');
|
|
242
|
-
console.log(' npm install exceljs
|
|
241
|
+
console.log('Установите @jtcsv/excel и exceljs для демонстрации Excel плагина:');
|
|
242
|
+
console.log(' npm install @jtcsv/excel exceljs');
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
// ============================================================================
|
|
@@ -277,4 +277,3 @@ console.log('✨ JTCSV 2.1.0 - Next Generation JSON/CSV Converter ✨');
|
|
|
277
277
|
console.log('✨'.repeat(30));
|
|
278
278
|
|
|
279
279
|
|
|
280
|
-
|
package/index.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ declare module 'jtcsv' {
|
|
|
44
44
|
parseBooleans?: boolean;
|
|
45
45
|
/** Maximum number of rows to process (optional, no limit by default) */
|
|
46
46
|
maxRows?: number;
|
|
47
|
+
/** Enable fast-path parsing (default: true) */
|
|
48
|
+
useFastPath?: boolean;
|
|
49
|
+
/** Fast-path output mode (default: 'objects') */
|
|
50
|
+
fastPathMode?: 'objects' | 'compact' | 'stream';
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// JSON save interfaces
|
|
@@ -71,6 +75,99 @@ declare module 'jtcsv' {
|
|
|
71
75
|
schema?: Record<string, any>;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
// NDJSON interfaces
|
|
79
|
+
export interface NdjsonOptions {
|
|
80
|
+
/** Buffer size for streaming (default: 64KB) */
|
|
81
|
+
bufferSize?: number;
|
|
82
|
+
/** Maximum line length (default: 10MB) */
|
|
83
|
+
maxLineLength?: number;
|
|
84
|
+
/** Error handler callback */
|
|
85
|
+
onError?: (error: Error, line: string, lineNumber: number) => void;
|
|
86
|
+
/** JSON stringify replacer function */
|
|
87
|
+
replacer?: (key: string, value: any) => any;
|
|
88
|
+
/** JSON stringify space (indentation) */
|
|
89
|
+
space?: number | string;
|
|
90
|
+
/** Filter function for rows */
|
|
91
|
+
filter?: (obj: Record<string, any>, index: number) => boolean;
|
|
92
|
+
/** Transform function for rows */
|
|
93
|
+
transform?: (obj: Record<string, any>, index: number) => any;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface NdjsonToCsvStreamOptions {
|
|
97
|
+
/** Delimiter for CSV output (default: ',') */
|
|
98
|
+
delimiter?: string;
|
|
99
|
+
/** Include headers row (default: true) */
|
|
100
|
+
includeHeaders?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface CsvToNdjsonStreamOptions {
|
|
104
|
+
/** Delimiter for CSV input (default: ',') */
|
|
105
|
+
delimiter?: string;
|
|
106
|
+
/** Whether CSV has headers (default: true) */
|
|
107
|
+
hasHeaders?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface NdjsonStats {
|
|
111
|
+
/** Total lines in NDJSON */
|
|
112
|
+
totalLines: number;
|
|
113
|
+
/** Valid JSON lines */
|
|
114
|
+
validLines: number;
|
|
115
|
+
/** Lines with JSON parsing errors */
|
|
116
|
+
errorLines: number;
|
|
117
|
+
/** Total bytes */
|
|
118
|
+
totalBytes: number;
|
|
119
|
+
/** Success rate percentage */
|
|
120
|
+
successRate: number;
|
|
121
|
+
/** Array of parsing errors */
|
|
122
|
+
errors: Array<{
|
|
123
|
+
line: number;
|
|
124
|
+
error: string;
|
|
125
|
+
content: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// TSV interfaces
|
|
130
|
+
export interface TsvOptions extends JsonToCsvOptions, CsvToJsonOptions {
|
|
131
|
+
/** Always use tab as delimiter for TSV */
|
|
132
|
+
delimiter?: '\t';
|
|
133
|
+
/** Disable auto-detection for TSV */
|
|
134
|
+
autoDetect?: false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ValidateTsvOptions {
|
|
138
|
+
/** Require consistent column count (default: true) */
|
|
139
|
+
requireConsistentColumns?: boolean;
|
|
140
|
+
/** Disallow empty fields (default: false) */
|
|
141
|
+
disallowEmptyFields?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface TsvValidationResult {
|
|
145
|
+
/** Whether TSV is valid */
|
|
146
|
+
valid: boolean;
|
|
147
|
+
/** Error message if invalid */
|
|
148
|
+
error?: string;
|
|
149
|
+
/** Validation statistics */
|
|
150
|
+
stats: {
|
|
151
|
+
/** Total lines */
|
|
152
|
+
totalLines: number;
|
|
153
|
+
/** Total columns in first line */
|
|
154
|
+
totalColumns: number;
|
|
155
|
+
/** Minimum columns across all lines */
|
|
156
|
+
minColumns: number;
|
|
157
|
+
/** Maximum columns across all lines */
|
|
158
|
+
maxColumns: number;
|
|
159
|
+
/** Whether all lines have same column count */
|
|
160
|
+
consistentColumns: boolean;
|
|
161
|
+
};
|
|
162
|
+
/** Array of validation errors */
|
|
163
|
+
errors?: Array<{
|
|
164
|
+
line?: number;
|
|
165
|
+
error: string;
|
|
166
|
+
details?: any;
|
|
167
|
+
fields?: number[];
|
|
168
|
+
}>;
|
|
169
|
+
}
|
|
170
|
+
|
|
74
171
|
// Error classes
|
|
75
172
|
export class JtcsvError extends Error {
|
|
76
173
|
code: string;
|
|
@@ -182,10 +279,31 @@ declare module 'jtcsv' {
|
|
|
182
279
|
* @throws {LimitError} If row limit exceeded
|
|
183
280
|
*/
|
|
184
281
|
export function csvToJson(
|
|
185
|
-
csv: string,
|
|
282
|
+
csv: string,
|
|
283
|
+
options: CsvToJsonOptions & { fastPathMode: 'stream' }
|
|
284
|
+
): AsyncGenerator<Record<string, any> | any[]>;
|
|
285
|
+
|
|
286
|
+
export function csvToJson(
|
|
287
|
+
csv: string,
|
|
288
|
+
options: CsvToJsonOptions & { fastPathMode: 'compact' }
|
|
289
|
+
): any[][];
|
|
290
|
+
|
|
291
|
+
export function csvToJson(
|
|
292
|
+
csv: string,
|
|
186
293
|
options?: CsvToJsonOptions
|
|
187
294
|
): Record<string, any>[];
|
|
188
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Convert CSV string to JSON rows as async iterator
|
|
298
|
+
* @param csv CSV string to convert
|
|
299
|
+
* @param options Conversion options
|
|
300
|
+
* @returns Async generator yielding rows
|
|
301
|
+
*/
|
|
302
|
+
export function csvToJsonIterator(
|
|
303
|
+
csv: string,
|
|
304
|
+
options?: CsvToJsonOptions
|
|
305
|
+
): AsyncGenerator<Record<string, any> | any[]>;
|
|
306
|
+
|
|
189
307
|
/**
|
|
190
308
|
* Read CSV file and convert it to JSON array
|
|
191
309
|
* @param filePath Path to CSV file
|
|
@@ -360,8 +478,173 @@ declare module 'jtcsv' {
|
|
|
360
478
|
* @returns Writable stream that collects data
|
|
361
479
|
*/
|
|
362
480
|
export function createJsonCollectorStream(): Writable;
|
|
481
|
+
|
|
482
|
+
// NDJSON format support
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Convert JSON array to NDJSON string
|
|
486
|
+
* @param data Array of objects to convert
|
|
487
|
+
* @param options NDJSON options
|
|
488
|
+
* @returns NDJSON string
|
|
489
|
+
*/
|
|
490
|
+
export function jsonToNdjson<T extends Record<string, any>>(
|
|
491
|
+
data: T[],
|
|
492
|
+
options?: NdjsonOptions
|
|
493
|
+
): string;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Convert NDJSON string to JSON array
|
|
497
|
+
* @param ndjsonString NDJSON string
|
|
498
|
+
* @param options NDJSON options
|
|
499
|
+
* @returns JSON array
|
|
500
|
+
*/
|
|
501
|
+
export function ndjsonToJson(
|
|
502
|
+
ndjsonString: string,
|
|
503
|
+
options?: NdjsonOptions
|
|
504
|
+
): Record<string, any>[];
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Parse NDJSON stream as async iterator
|
|
508
|
+
* @param input ReadableStream or string input
|
|
509
|
+
* @param options NDJSON options
|
|
510
|
+
* @returns Async generator of JSON objects
|
|
511
|
+
*/
|
|
512
|
+
export function parseNdjsonStream(
|
|
513
|
+
input: ReadableStream | string,
|
|
514
|
+
options?: NdjsonOptions
|
|
515
|
+
): AsyncGenerator<Record<string, any>>;
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Create TransformStream for converting NDJSON to CSV
|
|
519
|
+
* @param options Conversion options
|
|
520
|
+
* @returns TransformStream
|
|
521
|
+
*/
|
|
522
|
+
export function createNdjsonToCsvStream(
|
|
523
|
+
options?: NdjsonToCsvStreamOptions
|
|
524
|
+
): TransformStream;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Create TransformStream for converting CSV to NDJSON
|
|
528
|
+
* @param options Conversion options
|
|
529
|
+
* @returns TransformStream
|
|
530
|
+
*/
|
|
531
|
+
export function createCsvToNdjsonStream(
|
|
532
|
+
options?: CsvToNdjsonStreamOptions
|
|
533
|
+
): TransformStream;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Get statistics for NDJSON data
|
|
537
|
+
* @param input NDJSON string or ReadableStream
|
|
538
|
+
* @returns Promise with NDJSON statistics
|
|
539
|
+
*/
|
|
540
|
+
export function getNdjsonStats(
|
|
541
|
+
input: string | ReadableStream
|
|
542
|
+
): Promise<NdjsonStats>;
|
|
543
|
+
|
|
544
|
+
// TSV format support
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Convert JSON array to TSV string
|
|
548
|
+
* @param data Array of objects to convert
|
|
549
|
+
* @param options TSV options
|
|
550
|
+
* @returns TSV string
|
|
551
|
+
*/
|
|
552
|
+
export function jsonToTsv<T extends Record<string, any>>(
|
|
553
|
+
data: T[],
|
|
554
|
+
options?: TsvOptions
|
|
555
|
+
): string;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Convert TSV string to JSON array
|
|
559
|
+
* @param tsvString TSV string
|
|
560
|
+
* @param options TSV options
|
|
561
|
+
* @returns JSON array
|
|
562
|
+
*/
|
|
563
|
+
export function tsvToJson(
|
|
564
|
+
tsvString: string,
|
|
565
|
+
options?: TsvOptions
|
|
566
|
+
): Record<string, any>[];
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Check if string is likely TSV format
|
|
570
|
+
* @param sample Sample data string
|
|
571
|
+
* @returns True if likely TSV format
|
|
572
|
+
*/
|
|
573
|
+
export function isTsv(sample: string): boolean;
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Validate TSV string structure
|
|
577
|
+
* @param tsvString TSV string to validate
|
|
578
|
+
* @param options Validation options
|
|
579
|
+
* @returns Validation result
|
|
580
|
+
*/
|
|
581
|
+
export function validateTsv(
|
|
582
|
+
tsvString: string,
|
|
583
|
+
options?: ValidateTsvOptions
|
|
584
|
+
): TsvValidationResult;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Read TSV file and convert to JSON array
|
|
588
|
+
* @param filePath Path to TSV file
|
|
589
|
+
* @param options TSV options
|
|
590
|
+
* @returns Promise with JSON array
|
|
591
|
+
*/
|
|
592
|
+
export function readTsvAsJson(
|
|
593
|
+
filePath: string,
|
|
594
|
+
options?: TsvOptions
|
|
595
|
+
): Promise<Record<string, any>[]>;
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Synchronously read TSV file and convert to JSON array
|
|
599
|
+
* @param filePath Path to TSV file
|
|
600
|
+
* @param options TSV options
|
|
601
|
+
* @returns JSON array
|
|
602
|
+
*/
|
|
603
|
+
export function readTsvAsJsonSync(
|
|
604
|
+
filePath: string,
|
|
605
|
+
options?: TsvOptions
|
|
606
|
+
): Record<string, any>[];
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Save JSON data as TSV file
|
|
610
|
+
* @param data Array of objects to save
|
|
611
|
+
* @param filePath Output file path
|
|
612
|
+
* @param options TSV options
|
|
613
|
+
* @returns Promise that resolves when file is saved
|
|
614
|
+
*/
|
|
615
|
+
export function saveAsTsv<T extends Record<string, any>>(
|
|
616
|
+
data: T[],
|
|
617
|
+
filePath: string,
|
|
618
|
+
options?: TsvOptions
|
|
619
|
+
): Promise<void>;
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Synchronously save JSON data as TSV file
|
|
623
|
+
* @param data Array of objects to save
|
|
624
|
+
* @param filePath Output file path
|
|
625
|
+
* @param options TSV options
|
|
626
|
+
*/
|
|
627
|
+
export function saveAsTsvSync<T extends Record<string, any>>(
|
|
628
|
+
data: T[],
|
|
629
|
+
filePath: string,
|
|
630
|
+
options?: TsvOptions
|
|
631
|
+
): void;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Create TransformStream for converting JSON to TSV
|
|
635
|
+
* @param options TSV options
|
|
636
|
+
* @returns TransformStream
|
|
637
|
+
*/
|
|
638
|
+
export function createJsonToTsvStream(
|
|
639
|
+
options?: TsvOptions
|
|
640
|
+
): TransformStream;
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Create TransformStream for converting TSV to JSON
|
|
644
|
+
* @param options TSV options
|
|
645
|
+
* @returns TransformStream
|
|
646
|
+
*/
|
|
647
|
+
export function createTsvToJsonStream(
|
|
648
|
+
options?: TsvOptions
|
|
649
|
+
): TransformStream;
|
|
363
650
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
package/index.js
CHANGED
|
@@ -7,6 +7,8 @@ const errorsModule = require('./errors');
|
|
|
7
7
|
const jsonSaveModule = require('./json-save');
|
|
8
8
|
const streamJsonToCsvModule = require('./stream-json-to-csv');
|
|
9
9
|
const streamCsvToJsonModule = require('./stream-csv-to-json');
|
|
10
|
+
const ndjsonParser = require('./src/formats/ndjson-parser');
|
|
11
|
+
const tsvParser = require('./src/formats/tsv-parser');
|
|
10
12
|
|
|
11
13
|
// Combine all exports
|
|
12
14
|
module.exports = {
|
|
@@ -19,6 +21,7 @@ module.exports = {
|
|
|
19
21
|
|
|
20
22
|
// CSV to JSON functions
|
|
21
23
|
csvToJson: csvToJsonModule.csvToJson,
|
|
24
|
+
csvToJsonIterator: csvToJsonModule.csvToJsonIterator,
|
|
22
25
|
readCsvAsJson: csvToJsonModule.readCsvAsJson,
|
|
23
26
|
readCsvAsJsonSync: csvToJsonModule.readCsvAsJsonSync,
|
|
24
27
|
autoDetectDelimiter: csvToJsonModule.autoDetectDelimiter,
|
|
@@ -40,6 +43,26 @@ module.exports = {
|
|
|
40
43
|
createCsvFileToJsonStream: streamCsvToJsonModule.createCsvFileToJsonStream,
|
|
41
44
|
createJsonCollectorStream: streamCsvToJsonModule.createJsonCollectorStream,
|
|
42
45
|
|
|
46
|
+
// NDJSON format support
|
|
47
|
+
jsonToNdjson: ndjsonParser.toNdjson,
|
|
48
|
+
ndjsonToJson: ndjsonParser.fromNdjson,
|
|
49
|
+
parseNdjsonStream: ndjsonParser.parseStream,
|
|
50
|
+
createNdjsonToCsvStream: ndjsonParser.createNdjsonToCsvStream,
|
|
51
|
+
createCsvToNdjsonStream: ndjsonParser.createCsvToNdjsonStream,
|
|
52
|
+
getNdjsonStats: ndjsonParser.getStats,
|
|
53
|
+
|
|
54
|
+
// TSV format support
|
|
55
|
+
jsonToTsv: tsvParser.jsonToTsv,
|
|
56
|
+
tsvToJson: tsvParser.tsvToJson,
|
|
57
|
+
isTsv: tsvParser.isTsv,
|
|
58
|
+
validateTsv: tsvParser.validateTsv,
|
|
59
|
+
readTsvAsJson: tsvParser.readTsvAsJson,
|
|
60
|
+
readTsvAsJsonSync: tsvParser.readTsvAsJsonSync,
|
|
61
|
+
saveAsTsv: tsvParser.saveAsTsv,
|
|
62
|
+
saveAsTsvSync: tsvParser.saveAsTsvSync,
|
|
63
|
+
createJsonToTsvStream: tsvParser.createJsonToTsvStream,
|
|
64
|
+
createTsvToJsonStream: tsvParser.createTsvToJsonStream,
|
|
65
|
+
|
|
43
66
|
// Error classes
|
|
44
67
|
...errorsModule
|
|
45
68
|
};
|
package/json-to-csv.js
CHANGED
|
@@ -135,35 +135,68 @@ function jsonToCsv(data, options = {}) {
|
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// Get all unique keys from all objects
|
|
139
|
-
const allKeys = new Set();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
138
|
+
// Get all unique keys from all objects with minimal allocations.
|
|
139
|
+
const allKeys = new Set();
|
|
140
|
+
const originalKeys = [];
|
|
141
|
+
for (let i = 0; i < data.length; i++) {
|
|
142
|
+
const item = data[i];
|
|
143
|
+
if (!item || typeof item !== 'object') {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const key in item) {
|
|
147
|
+
if (Object.prototype.hasOwnProperty.call(item, key) && !allKeys.has(key)) {
|
|
148
|
+
allKeys.add(key);
|
|
149
|
+
originalKeys.push(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hasRenameMap = Object.keys(renameMap).length > 0;
|
|
155
|
+
const hasTemplate = Object.keys(template).length > 0;
|
|
156
|
+
|
|
157
|
+
// Apply rename map to create header names.
|
|
158
|
+
let headers = originalKeys;
|
|
159
|
+
let reverseRenameMap = null;
|
|
160
|
+
if (hasRenameMap) {
|
|
161
|
+
headers = new Array(originalKeys.length);
|
|
162
|
+
reverseRenameMap = {};
|
|
163
|
+
for (let i = 0; i < originalKeys.length; i++) {
|
|
164
|
+
const key = originalKeys[i];
|
|
165
|
+
const header = renameMap[key] || key;
|
|
166
|
+
headers[i] = header;
|
|
167
|
+
reverseRenameMap[header] = key;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Apply template ordering if provided.
|
|
172
|
+
let finalHeaders = headers;
|
|
173
|
+
if (hasTemplate) {
|
|
174
|
+
const templateKeys = Object.keys(template);
|
|
175
|
+
const templateHeaders = hasRenameMap
|
|
176
|
+
? templateKeys.map(key => renameMap[key] || key)
|
|
177
|
+
: templateKeys;
|
|
178
|
+
const templateHeaderSet = new Set(templateHeaders);
|
|
179
|
+
const extraHeaders = [];
|
|
180
|
+
for (let i = 0; i < headers.length; i++) {
|
|
181
|
+
const header = headers[i];
|
|
182
|
+
if (!templateHeaderSet.has(header)) {
|
|
183
|
+
extraHeaders.push(header);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finalHeaders = templateHeaders.concat(extraHeaders);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const finalKeys = new Array(finalHeaders.length);
|
|
190
|
+
if (hasRenameMap) {
|
|
191
|
+
for (let i = 0; i < finalHeaders.length; i++) {
|
|
192
|
+
const header = finalHeaders[i];
|
|
193
|
+
finalKeys[i] = reverseRenameMap[header] || header;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
for (let i = 0; i < finalHeaders.length; i++) {
|
|
197
|
+
finalKeys[i] = finalHeaders[i];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
167
200
|
|
|
168
201
|
/**
|
|
169
202
|
* Escapes a value for CSV format with CSV injection protection
|
|
@@ -172,67 +205,75 @@ function jsonToCsv(data, options = {}) {
|
|
|
172
205
|
* @param {*} value - The value to escape
|
|
173
206
|
* @returns {string} Escaped CSV value
|
|
174
207
|
*/
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
const quoteRegex = /"/g;
|
|
209
|
+
const delimiterCode = delimiter.charCodeAt(0);
|
|
210
|
+
|
|
211
|
+
const escapeValue = (value) => {
|
|
212
|
+
if (value === null || value === undefined || value === '') {
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let stringValue = value;
|
|
217
|
+
if (typeof stringValue !== 'string') {
|
|
218
|
+
stringValue = String(stringValue);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// CSV Injection protection - escape formulas if enabled
|
|
222
|
+
let escapedValue = stringValue;
|
|
223
|
+
if (preventCsvInjection) {
|
|
224
|
+
const firstCharCode = stringValue.charCodeAt(0);
|
|
225
|
+
if (firstCharCode === 61 || firstCharCode === 43 || firstCharCode === 45 || firstCharCode === 64) {
|
|
226
|
+
escapedValue = "'" + stringValue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let needsQuoting = false;
|
|
231
|
+
let hasQuote = false;
|
|
232
|
+
for (let i = 0; i < escapedValue.length; i++) {
|
|
233
|
+
const code = escapedValue.charCodeAt(i);
|
|
234
|
+
if (code === 34) {
|
|
235
|
+
hasQuote = true;
|
|
236
|
+
needsQuoting = true;
|
|
237
|
+
} else if (code === delimiterCode || code === 10 || code === 13) {
|
|
238
|
+
needsQuoting = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (needsQuoting) {
|
|
243
|
+
const quotedValue = hasQuote ? escapedValue.replace(quoteRegex, '""') : escapedValue;
|
|
244
|
+
return `"${quotedValue}"`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return escapedValue;
|
|
248
|
+
};
|
|
208
249
|
|
|
209
|
-
// Build CSV rows
|
|
210
|
-
const rows = [];
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
rows.push(
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// RFC 4180: Each record is located on a separate line, delimited by a line break (CRLF)
|
|
234
|
-
const lineEnding = rfc4180Compliant ? '\r\n' : '\n';
|
|
235
|
-
return rows.join(lineEnding);
|
|
250
|
+
// Build CSV rows.
|
|
251
|
+
const rows = [];
|
|
252
|
+
const columnCount = finalHeaders.length;
|
|
253
|
+
|
|
254
|
+
// Add headers row if requested.
|
|
255
|
+
if (includeHeaders && columnCount > 0) {
|
|
256
|
+
rows.push(finalHeaders.join(delimiter));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add data rows.
|
|
260
|
+
const rowValues = new Array(columnCount);
|
|
261
|
+
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
262
|
+
const item = data[rowIndex];
|
|
263
|
+
if (!item || typeof item !== 'object') {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < columnCount; i++) {
|
|
268
|
+
rowValues[i] = escapeValue(item[finalKeys[i]]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
rows.push(rowValues.join(delimiter));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// RFC 4180: Each record is located on a separate line, delimited by a line break (CRLF).
|
|
275
|
+
const lineEnding = rfc4180Compliant ? '\r\n' : '\n';
|
|
276
|
+
return rows.join(lineEnding);
|
|
236
277
|
}, 'PARSE_FAILED', { function: 'jsonToCsv' });
|
|
237
278
|
}
|
|
238
279
|
|