jtcsv 2.2.7 → 3.0.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 +31 -1
- package/bin/jtcsv.js +891 -821
- package/bin/jtcsv.ts +2534 -0
- package/csv-to-json.js +168 -145
- package/dist/jtcsv-core.cjs.js +1407 -0
- package/dist/jtcsv-core.cjs.js.map +1 -0
- package/dist/jtcsv-core.esm.js +1379 -0
- package/dist/jtcsv-core.esm.js.map +1 -0
- package/dist/jtcsv-core.umd.js +1413 -0
- package/dist/jtcsv-core.umd.js.map +1 -0
- package/dist/jtcsv-full.cjs.js +1912 -0
- package/dist/jtcsv-full.cjs.js.map +1 -0
- package/dist/jtcsv-full.esm.js +1880 -0
- package/dist/jtcsv-full.esm.js.map +1 -0
- package/dist/jtcsv-full.umd.js +1918 -0
- package/dist/jtcsv-full.umd.js.map +1 -0
- package/dist/jtcsv-workers.esm.js +759 -0
- package/dist/jtcsv-workers.esm.js.map +1 -0
- package/dist/jtcsv-workers.umd.js +773 -0
- package/dist/jtcsv-workers.umd.js.map +1 -0
- package/dist/jtcsv.cjs.js +61 -19
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +61 -19
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +61 -19
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +188 -2
- package/examples/advanced/conditional-transformations.js +446 -0
- package/examples/advanced/conditional-transformations.ts +446 -0
- package/examples/advanced/csv-parser.worker.js +89 -0
- package/examples/advanced/csv-parser.worker.ts +89 -0
- package/examples/advanced/nested-objects-example.js +306 -0
- package/examples/advanced/nested-objects-example.ts +306 -0
- package/examples/advanced/performance-optimization.js +504 -0
- package/examples/advanced/performance-optimization.ts +504 -0
- package/examples/advanced/run-demo-server.js +116 -0
- package/examples/advanced/run-demo-server.ts +116 -0
- package/examples/advanced/web-worker-usage.html +874 -0
- package/examples/async-multithreaded-example.ts +335 -0
- package/examples/cli-advanced-usage.md +288 -0
- package/examples/cli-batch-processing.ts +38 -0
- package/examples/cli-tool.js +0 -3
- package/examples/cli-tool.ts +183 -0
- package/examples/error-handling.js +21 -7
- package/examples/error-handling.ts +356 -0
- package/examples/express-api.js +0 -3
- package/examples/express-api.ts +164 -0
- package/examples/large-dataset-example.js +0 -3
- package/examples/large-dataset-example.ts +204 -0
- package/examples/ndjson-processing.js +1 -1
- package/examples/ndjson-processing.ts +456 -0
- package/examples/plugin-excel-exporter.js +3 -4
- package/examples/plugin-excel-exporter.ts +406 -0
- package/examples/react-integration.tsx +637 -0
- package/examples/schema-validation.ts +640 -0
- package/examples/simple-usage.js +254 -254
- package/examples/simple-usage.ts +194 -0
- package/examples/streaming-example.js +4 -5
- package/examples/streaming-example.ts +419 -0
- package/examples/web-workers-advanced.ts +28 -0
- package/index.d.ts +1 -3
- package/index.js +15 -1
- package/json-save.js +9 -3
- package/json-to-csv.js +168 -21
- package/package.json +69 -10
- package/plugins/express-middleware/README.md +21 -2
- package/plugins/express-middleware/example.js +3 -4
- package/plugins/express-middleware/example.ts +135 -0
- package/plugins/express-middleware/index.d.ts +1 -1
- package/plugins/express-middleware/index.js +270 -118
- package/plugins/express-middleware/index.ts +557 -0
- package/plugins/fastify-plugin/index.js +2 -4
- package/plugins/fastify-plugin/index.ts +443 -0
- package/plugins/hono/index.ts +226 -0
- package/plugins/nestjs/index.ts +201 -0
- package/plugins/nextjs-api/examples/ConverterComponent.tsx +386 -0
- package/plugins/nextjs-api/examples/api-convert.js +0 -2
- package/plugins/nextjs-api/examples/api-convert.ts +67 -0
- package/plugins/nextjs-api/index.tsx +339 -0
- package/plugins/nextjs-api/route.js +2 -3
- package/plugins/nextjs-api/route.ts +370 -0
- package/plugins/nuxt/index.ts +94 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.ts +100 -0
- package/plugins/nuxt/runtime/plugin.ts +71 -0
- package/plugins/remix/index.js +1 -1
- package/plugins/remix/index.ts +260 -0
- package/plugins/sveltekit/index.js +1 -1
- package/plugins/sveltekit/index.ts +301 -0
- package/plugins/trpc/index.ts +267 -0
- package/src/browser/browser-functions.ts +402 -0
- package/src/browser/core.js +92 -0
- package/src/browser/core.ts +152 -0
- package/src/browser/csv-to-json-browser.d.ts +3 -0
- package/src/browser/csv-to-json-browser.js +36 -14
- package/src/browser/csv-to-json-browser.ts +264 -0
- package/src/browser/errors-browser.ts +303 -0
- package/src/browser/extensions/plugins.js +92 -0
- package/src/browser/extensions/plugins.ts +93 -0
- package/src/browser/extensions/workers.js +39 -0
- package/src/browser/extensions/workers.ts +39 -0
- package/src/browser/globals.d.ts +5 -0
- package/src/browser/index.ts +192 -0
- package/src/browser/json-to-csv-browser.d.ts +3 -0
- package/src/browser/json-to-csv-browser.js +13 -3
- package/src/browser/json-to-csv-browser.ts +262 -0
- package/src/browser/streams.js +12 -2
- package/src/browser/streams.ts +336 -0
- package/src/browser/workers/csv-parser.worker.ts +377 -0
- package/src/browser/workers/worker-pool.ts +548 -0
- package/src/core/delimiter-cache.js +22 -8
- package/src/core/delimiter-cache.ts +310 -0
- package/src/core/node-optimizations.ts +449 -0
- package/src/core/plugin-system.js +29 -11
- package/src/core/plugin-system.ts +400 -0
- package/src/core/transform-hooks.ts +558 -0
- package/src/engines/fast-path-engine-new.ts +347 -0
- package/src/engines/fast-path-engine.ts +854 -0
- package/src/errors.ts +72 -0
- package/src/formats/ndjson-parser.ts +469 -0
- package/src/formats/tsv-parser.ts +334 -0
- package/src/index-with-plugins.js +16 -9
- package/src/index-with-plugins.ts +395 -0
- package/src/types/index.ts +255 -0
- package/src/utils/bom-utils.js +259 -0
- package/src/utils/bom-utils.ts +373 -0
- package/src/utils/encoding-support.js +124 -0
- package/src/utils/encoding-support.ts +155 -0
- package/src/utils/schema-validator.js +19 -19
- package/src/utils/schema-validator.ts +819 -0
- package/src/utils/transform-loader.js +1 -1
- package/src/utils/transform-loader.ts +389 -0
- package/src/utils/zod-adapter.js +170 -0
- package/src/utils/zod-adapter.ts +280 -0
- package/src/web-server/index.js +10 -10
- package/src/web-server/index.ts +683 -0
- package/src/workers/csv-multithreaded.ts +310 -0
- package/src/workers/csv-parser.worker.ts +227 -0
- package/src/workers/worker-pool.ts +409 -0
- package/stream-csv-to-json.js +26 -8
- package/stream-json-to-csv.js +1 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Браузерный entry point для jtcsv
|
|
2
|
+
// Экспортирует все функции с поддержкой браузера
|
|
3
|
+
|
|
4
|
+
import * as jsonToCsvBrowser from './json-to-csv-browser';
|
|
5
|
+
import * as csvToJsonBrowser from './csv-to-json-browser';
|
|
6
|
+
import {
|
|
7
|
+
downloadAsCsv,
|
|
8
|
+
parseCsvFile,
|
|
9
|
+
parseCsvFileStream,
|
|
10
|
+
jsonToCsvStream,
|
|
11
|
+
jsonToNdjsonStream,
|
|
12
|
+
csvToJsonStream
|
|
13
|
+
} from './browser-functions';
|
|
14
|
+
import { createWorkerPool, parseCSVWithWorker } from './workers/worker-pool';
|
|
15
|
+
import {
|
|
16
|
+
ValidationError,
|
|
17
|
+
SecurityError,
|
|
18
|
+
FileSystemError,
|
|
19
|
+
ParsingError,
|
|
20
|
+
LimitError,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
ERROR_CODES
|
|
23
|
+
} from './errors-browser';
|
|
24
|
+
|
|
25
|
+
import type { JsonToCsvOptions, CsvToJsonOptions } from '../types';
|
|
26
|
+
|
|
27
|
+
const { jsonToCsv, preprocessData, deepUnwrap } = jsonToCsvBrowser as any;
|
|
28
|
+
const { csvToJson, csvToJsonIterator, autoDetectDelimiter } = csvToJsonBrowser as any;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Опции для ленивой инициализации Worker Pool
|
|
32
|
+
*/
|
|
33
|
+
interface WorkerPoolOptions {
|
|
34
|
+
size?: number;
|
|
35
|
+
timeout?: number;
|
|
36
|
+
onError?: (error: Error) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ленивая инициализация Worker Pool
|
|
41
|
+
*/
|
|
42
|
+
async function createWorkerPoolLazy(options: any = {}): Promise<any> {
|
|
43
|
+
const mod = await import('./workers/worker-pool');
|
|
44
|
+
return mod.createWorkerPool(options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ленивый парсинг CSV с использованием Worker
|
|
49
|
+
*/
|
|
50
|
+
async function parseCSVWithWorkerLazy(
|
|
51
|
+
csvInput: string | File,
|
|
52
|
+
options: CsvToJsonOptions = {},
|
|
53
|
+
onProgress?: (progress: number) => void
|
|
54
|
+
): Promise<any[]> {
|
|
55
|
+
const mod = await import('./workers/worker-pool');
|
|
56
|
+
return mod.parseCSVWithWorker(csvInput, options, onProgress);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Асинхронная версия jsonToCsv
|
|
61
|
+
*/
|
|
62
|
+
async function jsonToCsvAsync(data: any, options: JsonToCsvOptions = {}): Promise<string> {
|
|
63
|
+
return jsonToCsv(data, options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Асинхронная версия csvToJson
|
|
68
|
+
*/
|
|
69
|
+
async function csvToJsonAsync(csv: string, options: CsvToJsonOptions = {}): Promise<any[]> {
|
|
70
|
+
return csvToJson(csv, options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Асинхронная версия parseCsvFile
|
|
75
|
+
*/
|
|
76
|
+
async function parseCsvFileAsync(file: File, options: CsvToJsonOptions = {}): Promise<any[]> {
|
|
77
|
+
return parseCsvFile(file, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Асинхронная версия autoDetectDelimiter
|
|
82
|
+
*/
|
|
83
|
+
async function autoDetectDelimiterAsync(csv: string): Promise<string> {
|
|
84
|
+
return autoDetectDelimiter(csv);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Асинхронная версия downloadAsCsv
|
|
89
|
+
*/
|
|
90
|
+
async function downloadAsCsvAsync(
|
|
91
|
+
data: any,
|
|
92
|
+
filename: string = 'export.csv',
|
|
93
|
+
options: JsonToCsvOptions = {}
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
return downloadAsCsv(data, filename, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Основной экспорт
|
|
99
|
+
const jtcsv = {
|
|
100
|
+
// JSON to CSV функции
|
|
101
|
+
jsonToCsv,
|
|
102
|
+
preprocessData,
|
|
103
|
+
downloadAsCsv,
|
|
104
|
+
deepUnwrap,
|
|
105
|
+
|
|
106
|
+
// CSV to JSON функции
|
|
107
|
+
csvToJson,
|
|
108
|
+
csvToJsonIterator,
|
|
109
|
+
parseCsvFile,
|
|
110
|
+
parseCsvFileStream,
|
|
111
|
+
jsonToCsvStream,
|
|
112
|
+
jsonToNdjsonStream,
|
|
113
|
+
csvToJsonStream,
|
|
114
|
+
autoDetectDelimiter,
|
|
115
|
+
|
|
116
|
+
// Web Workers функции
|
|
117
|
+
createWorkerPool,
|
|
118
|
+
parseCSVWithWorker,
|
|
119
|
+
createWorkerPoolLazy,
|
|
120
|
+
parseCSVWithWorkerLazy,
|
|
121
|
+
|
|
122
|
+
// Асинхронные функции
|
|
123
|
+
jsonToCsvAsync,
|
|
124
|
+
csvToJsonAsync,
|
|
125
|
+
parseCsvFileAsync,
|
|
126
|
+
autoDetectDelimiterAsync,
|
|
127
|
+
downloadAsCsvAsync,
|
|
128
|
+
|
|
129
|
+
// Error classes
|
|
130
|
+
ValidationError,
|
|
131
|
+
SecurityError,
|
|
132
|
+
FileSystemError,
|
|
133
|
+
ParsingError,
|
|
134
|
+
LimitError,
|
|
135
|
+
ConfigurationError,
|
|
136
|
+
ERROR_CODES,
|
|
137
|
+
|
|
138
|
+
// Удобные алиасы
|
|
139
|
+
parse: csvToJson,
|
|
140
|
+
unparse: jsonToCsv,
|
|
141
|
+
parseAsync: csvToJsonAsync,
|
|
142
|
+
unparseAsync: jsonToCsvAsync,
|
|
143
|
+
|
|
144
|
+
// Версия
|
|
145
|
+
version: '2.0.0-browser'
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Экспорт для разных сред
|
|
149
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
150
|
+
// Node.js CommonJS
|
|
151
|
+
module.exports = jtcsv;
|
|
152
|
+
} else if (typeof define === 'function' && define.amd) {
|
|
153
|
+
// AMD
|
|
154
|
+
define([], () => jtcsv);
|
|
155
|
+
} else if (typeof window !== 'undefined') {
|
|
156
|
+
// Браузер (глобальная переменная)
|
|
157
|
+
(window as any).jtcsv = jtcsv;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default jtcsv;
|
|
161
|
+
export {
|
|
162
|
+
jsonToCsv,
|
|
163
|
+
preprocessData,
|
|
164
|
+
downloadAsCsv,
|
|
165
|
+
deepUnwrap,
|
|
166
|
+
csvToJson,
|
|
167
|
+
csvToJsonIterator,
|
|
168
|
+
parseCsvFile,
|
|
169
|
+
parseCsvFileStream,
|
|
170
|
+
jsonToCsvStream,
|
|
171
|
+
jsonToNdjsonStream,
|
|
172
|
+
csvToJsonStream,
|
|
173
|
+
autoDetectDelimiter,
|
|
174
|
+
createWorkerPool,
|
|
175
|
+
parseCSVWithWorker,
|
|
176
|
+
createWorkerPoolLazy,
|
|
177
|
+
parseCSVWithWorkerLazy,
|
|
178
|
+
// Асинхронные функции
|
|
179
|
+
jsonToCsvAsync,
|
|
180
|
+
csvToJsonAsync,
|
|
181
|
+
parseCsvFileAsync,
|
|
182
|
+
autoDetectDelimiterAsync,
|
|
183
|
+
downloadAsCsvAsync,
|
|
184
|
+
// Error classes
|
|
185
|
+
ValidationError,
|
|
186
|
+
SecurityError,
|
|
187
|
+
FileSystemError,
|
|
188
|
+
ParsingError,
|
|
189
|
+
LimitError,
|
|
190
|
+
ConfigurationError,
|
|
191
|
+
ERROR_CODES
|
|
192
|
+
};
|
|
@@ -152,8 +152,18 @@ export function jsonToCsv(data, options = {}) {
|
|
|
152
152
|
|
|
153
153
|
// Защита от CSV инъекций
|
|
154
154
|
let escapedValue = stringValue;
|
|
155
|
-
if (preventCsvInjection
|
|
156
|
-
|
|
155
|
+
if (preventCsvInjection) {
|
|
156
|
+
// Dangerous prefixes: =, +, -, @, tab (\t), carriage return (\r)
|
|
157
|
+
if (/^[=+\-@\t\r]/.test(stringValue)) {
|
|
158
|
+
escapedValue = "'" + stringValue;
|
|
159
|
+
}
|
|
160
|
+
// Unicode Bidi override characters
|
|
161
|
+
const bidiChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
|
|
162
|
+
for (const bidi of bidiChars) {
|
|
163
|
+
if (stringValue.includes(bidi)) {
|
|
164
|
+
escapedValue = escapedValue.replace(new RegExp(bidi, 'g'), '');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
// Соответствие RFC 4180
|
|
@@ -306,4 +316,4 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
306
316
|
preprocessData,
|
|
307
317
|
deepUnwrap
|
|
308
318
|
};
|
|
309
|
-
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Браузерная версия JSON to CSV конвертера
|
|
2
|
+
// Адаптирована для работы в браузере без Node.js API
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ValidationError,
|
|
6
|
+
ConfigurationError,
|
|
7
|
+
LimitError,
|
|
8
|
+
safeExecute
|
|
9
|
+
} from './errors-browser';
|
|
10
|
+
|
|
11
|
+
import type { JsonToCsvOptions } from '../types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Валидация входных данных и опций
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
function validateInput(data: any[], options: JsonToCsvOptions): boolean {
|
|
18
|
+
// Validate data
|
|
19
|
+
if (!Array.isArray(data)) {
|
|
20
|
+
throw new ValidationError('Input data must be an array');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate options
|
|
24
|
+
if (options && typeof options !== 'object') {
|
|
25
|
+
throw new ConfigurationError('Options must be an object');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validate delimiter
|
|
29
|
+
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
30
|
+
throw new ConfigurationError('Delimiter must be a string');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
34
|
+
throw new ConfigurationError('Delimiter must be a single character');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate renameMap
|
|
38
|
+
if (options?.renameMap && typeof options.renameMap !== 'object') {
|
|
39
|
+
throw new ConfigurationError('renameMap must be an object');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate maxRecords
|
|
43
|
+
if (options && options.maxRecords !== undefined) {
|
|
44
|
+
if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
|
|
45
|
+
throw new ConfigurationError('maxRecords must be a positive number');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate preventCsvInjection
|
|
50
|
+
if (options?.preventCsvInjection !== undefined && typeof options.preventCsvInjection !== 'boolean') {
|
|
51
|
+
throw new ConfigurationError('preventCsvInjection must be a boolean');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate rfc4180Compliant
|
|
55
|
+
if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
|
|
56
|
+
throw new ConfigurationError('rfc4180Compliant must be a boolean');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Экранирование CSV значений для предотвращения инъекций
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
function escapeCsvValue(value: string, preventInjection: boolean = true): string {
|
|
67
|
+
if (value === null || value === undefined) {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const str = String(value);
|
|
72
|
+
|
|
73
|
+
// Экранирование формул для предотвращения CSV инъекций
|
|
74
|
+
if (preventInjection && /^[=+\-@]/.test(str)) {
|
|
75
|
+
return "'" + str;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Экранирование кавычек и переносов строк
|
|
79
|
+
if (str.includes('"') || str.includes('\n') || str.includes('\r') || str.includes(',')) {
|
|
80
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return str;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Извлечение всех уникальных ключей из массива объектов
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
function extractAllKeys(data: any[]): string[] {
|
|
91
|
+
const keys = new Set<string>();
|
|
92
|
+
|
|
93
|
+
for (const item of data) {
|
|
94
|
+
if (item && typeof item === 'object') {
|
|
95
|
+
Object.keys(item).forEach(key => keys.add(key));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Array.from(keys);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Конвертация массива объектов в CSV строку
|
|
104
|
+
*
|
|
105
|
+
* @param data - Массив объектов для конвертации
|
|
106
|
+
* @param options - Опции конвертации
|
|
107
|
+
* @returns CSV строка
|
|
108
|
+
*/
|
|
109
|
+
export function jsonToCsv(data: any[], options: JsonToCsvOptions = {}): string {
|
|
110
|
+
return safeExecute(() => {
|
|
111
|
+
validateInput(data, options);
|
|
112
|
+
|
|
113
|
+
if (data.length === 0) {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Настройки по умолчанию
|
|
118
|
+
const delimiter = options.delimiter || ';';
|
|
119
|
+
const includeHeaders = options.includeHeaders !== false;
|
|
120
|
+
const maxRecords = options.maxRecords || data.length;
|
|
121
|
+
const preventInjection = options.preventCsvInjection !== false;
|
|
122
|
+
const rfc4180Compliant = options.rfc4180Compliant !== false;
|
|
123
|
+
|
|
124
|
+
// Ограничение количества записей
|
|
125
|
+
const limitedData = data.slice(0, maxRecords);
|
|
126
|
+
|
|
127
|
+
// Извлечение всех ключей
|
|
128
|
+
const allKeys = extractAllKeys(limitedData);
|
|
129
|
+
|
|
130
|
+
// Применение renameMap если есть
|
|
131
|
+
const renameMap = options.renameMap || {};
|
|
132
|
+
const finalKeys = allKeys.map(key => renameMap[key] || key);
|
|
133
|
+
|
|
134
|
+
// Создание CSV строки
|
|
135
|
+
const lines: string[] = [];
|
|
136
|
+
|
|
137
|
+
// Заголовки
|
|
138
|
+
if (includeHeaders) {
|
|
139
|
+
const headerLine = finalKeys.map(key => escapeCsvValue(key, preventInjection)).join(delimiter);
|
|
140
|
+
lines.push(headerLine);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Данные
|
|
144
|
+
for (const item of limitedData) {
|
|
145
|
+
const rowValues = allKeys.map(key => {
|
|
146
|
+
const value = item?.[key];
|
|
147
|
+
return escapeCsvValue(value, preventInjection);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
lines.push(rowValues.join(delimiter));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// RFC 4180 compliance: CRLF line endings
|
|
154
|
+
if (rfc4180Compliant) {
|
|
155
|
+
return lines.join('\r\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Асинхронная версия jsonToCsv
|
|
164
|
+
*/
|
|
165
|
+
export async function jsonToCsvAsync(data: any[], options: JsonToCsvOptions = {}): Promise<string> {
|
|
166
|
+
return jsonToCsv(data, options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Создает итератор для потоковой конвертации JSON в CSV
|
|
171
|
+
*
|
|
172
|
+
* @param data - Массив объектов или async итератор
|
|
173
|
+
* @param options - Опции конвертации
|
|
174
|
+
* @returns AsyncIterator с CSV чанками
|
|
175
|
+
*/
|
|
176
|
+
export async function* jsonToCsvIterator(data: any[] | AsyncIterable<any>, options: JsonToCsvOptions = {}): AsyncGenerator<string> {
|
|
177
|
+
validateInput(Array.isArray(data) ? data : [], options);
|
|
178
|
+
|
|
179
|
+
const delimiter = options.delimiter || ';';
|
|
180
|
+
const includeHeaders = options.includeHeaders !== false;
|
|
181
|
+
const preventInjection = options.preventCsvInjection !== false;
|
|
182
|
+
const rfc4180Compliant = options.rfc4180Compliant !== false;
|
|
183
|
+
|
|
184
|
+
let isFirstChunk = true;
|
|
185
|
+
let allKeys: string[] = [];
|
|
186
|
+
let renameMap: Record<string, string> = {};
|
|
187
|
+
|
|
188
|
+
// Если данные - массив, обрабатываем как массив
|
|
189
|
+
if (Array.isArray(data)) {
|
|
190
|
+
if (data.length === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
allKeys = extractAllKeys(data);
|
|
195
|
+
renameMap = options.renameMap || {};
|
|
196
|
+
const finalKeys = allKeys.map(key => renameMap[key] || key);
|
|
197
|
+
|
|
198
|
+
// Заголовки
|
|
199
|
+
if (includeHeaders) {
|
|
200
|
+
const headerLine = finalKeys.map(key => escapeCsvValue(key, preventInjection)).join(delimiter);
|
|
201
|
+
yield headerLine + (rfc4180Compliant ? '\r\n' : '\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Данные
|
|
205
|
+
for (const item of data) {
|
|
206
|
+
const rowValues = allKeys.map(key => {
|
|
207
|
+
const value = item?.[key];
|
|
208
|
+
return escapeCsvValue(value, preventInjection);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
yield rowValues.join(delimiter) + (rfc4180Compliant ? '\r\n' : '\n');
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Для async итератора нужна другая логика
|
|
215
|
+
throw new ValidationError('Async iterators not yet implemented in browser version');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Асинхронная версия jsonToCsvIterator (псевдоним)
|
|
221
|
+
*/
|
|
222
|
+
export const jsonToCsvIteratorAsync = jsonToCsvIterator;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Безопасная конвертация с обработкой ошибок
|
|
226
|
+
*
|
|
227
|
+
* @param data - Массив объектов
|
|
228
|
+
* @param options - Опции конвертации
|
|
229
|
+
* @returns CSV строка или null при ошибке
|
|
230
|
+
*/
|
|
231
|
+
export function jsonToCsvSafe(data: any[], options: JsonToCsvOptions = {}): string | null {
|
|
232
|
+
try {
|
|
233
|
+
return jsonToCsv(data, options);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('JSON to CSV conversion error:', error);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Асинхронная версия jsonToCsvSafe
|
|
242
|
+
*/
|
|
243
|
+
export async function jsonToCsvSafeAsync(data: any[], options: JsonToCsvOptions = {}): Promise<string | null> {
|
|
244
|
+
try {
|
|
245
|
+
return await jsonToCsvAsync(data, options);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('JSON to CSV conversion error:', error);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Экспорт для Node.js совместимости
|
|
253
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
254
|
+
module.exports = {
|
|
255
|
+
jsonToCsv,
|
|
256
|
+
jsonToCsvAsync,
|
|
257
|
+
jsonToCsvIterator,
|
|
258
|
+
jsonToCsvIteratorAsync,
|
|
259
|
+
jsonToCsvSafe,
|
|
260
|
+
jsonToCsvSafeAsync
|
|
261
|
+
};
|
|
262
|
+
}
|
package/src/browser/streams.js
CHANGED
|
@@ -211,8 +211,18 @@ function escapeCsvValue(value, options) {
|
|
|
211
211
|
|
|
212
212
|
const stringValue = String(value);
|
|
213
213
|
let escapedValue = stringValue;
|
|
214
|
-
if (preventCsvInjection
|
|
215
|
-
|
|
214
|
+
if (preventCsvInjection) {
|
|
215
|
+
// Dangerous prefixes: =, +, -, @, tab (\t), carriage return (\r)
|
|
216
|
+
if (/^[=+\-@\t\r]/.test(stringValue)) {
|
|
217
|
+
escapedValue = "'" + stringValue;
|
|
218
|
+
}
|
|
219
|
+
// Unicode Bidi override characters
|
|
220
|
+
const bidiChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
|
|
221
|
+
for (const bidi of bidiChars) {
|
|
222
|
+
if (stringValue.includes(bidi)) {
|
|
223
|
+
escapedValue = escapedValue.replace(new RegExp(bidi, 'g'), '');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
216
226
|
}
|
|
217
227
|
|
|
218
228
|
const needsQuoting = rfc4180Compliant
|