jtcsv 3.0.0 → 3.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 +205 -146
- package/bin/jtcsv.ts +280 -202
- package/browser.d.ts +142 -0
- package/dist/benchmark.js +446 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/bin/jtcsv.js +1940 -0
- package/dist/bin/jtcsv.js.map +1 -0
- package/dist/csv-to-json.js +1262 -0
- package/dist/csv-to-json.js.map +1 -0
- package/dist/errors.js +291 -0
- package/dist/errors.js.map +1 -0
- package/dist/eslint.config.js +147 -0
- package/dist/eslint.config.js.map +1 -0
- package/dist/index-core.js +95 -0
- package/dist/index-core.js.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/json-save.js +229 -0
- package/dist/json-save.js.map +1 -0
- package/dist/json-to-csv.js +576 -0
- package/dist/json-to-csv.js.map +1 -0
- package/dist/jtcsv-core.cjs.js +336 -7
- package/dist/jtcsv-core.cjs.js.map +1 -1
- package/dist/jtcsv-core.esm.js +336 -7
- package/dist/jtcsv-core.esm.js.map +1 -1
- package/dist/jtcsv-core.umd.js +336 -7
- package/dist/jtcsv-core.umd.js.map +1 -1
- package/dist/jtcsv-full.cjs.js +336 -7
- package/dist/jtcsv-full.cjs.js.map +1 -1
- package/dist/jtcsv-full.esm.js +336 -7
- package/dist/jtcsv-full.esm.js.map +1 -1
- package/dist/jtcsv-full.umd.js +336 -7
- package/dist/jtcsv-full.umd.js.map +1 -1
- package/dist/jtcsv-workers.esm.js +9 -0
- package/dist/jtcsv-workers.esm.js.map +1 -1
- package/dist/jtcsv-workers.umd.js +9 -0
- package/dist/jtcsv-workers.umd.js.map +1 -1
- package/dist/jtcsv.cjs.js +1998 -2092
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +1994 -2092
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +2157 -2251
- package/dist/jtcsv.umd.js.map +1 -1
- package/dist/plugins/express-middleware/index.js +350 -0
- package/dist/plugins/express-middleware/index.js.map +1 -0
- package/dist/plugins/fastify-plugin/index.js +315 -0
- package/dist/plugins/fastify-plugin/index.js.map +1 -0
- package/dist/plugins/hono/index.js +111 -0
- package/dist/plugins/hono/index.js.map +1 -0
- package/dist/plugins/nestjs/index.js +112 -0
- package/dist/plugins/nestjs/index.js.map +1 -0
- package/dist/plugins/nuxt/index.js +53 -0
- package/dist/plugins/nuxt/index.js.map +1 -0
- package/dist/plugins/remix/index.js +133 -0
- package/dist/plugins/remix/index.js.map +1 -0
- package/dist/plugins/sveltekit/index.js +155 -0
- package/dist/plugins/sveltekit/index.js.map +1 -0
- package/dist/plugins/trpc/index.js +136 -0
- package/dist/plugins/trpc/index.js.map +1 -0
- package/dist/run-demo.js +49 -0
- package/dist/run-demo.js.map +1 -0
- package/dist/src/browser/browser-functions.js +193 -0
- package/dist/src/browser/browser-functions.js.map +1 -0
- package/dist/src/browser/core.js +123 -0
- package/dist/src/browser/core.js.map +1 -0
- package/dist/src/browser/csv-to-json-browser.js +353 -0
- package/dist/src/browser/csv-to-json-browser.js.map +1 -0
- package/dist/src/browser/errors-browser.js +219 -0
- package/dist/src/browser/errors-browser.js.map +1 -0
- package/dist/src/browser/extensions/plugins.js +106 -0
- package/dist/src/browser/extensions/plugins.js.map +1 -0
- package/dist/src/browser/extensions/workers.js +66 -0
- package/dist/src/browser/extensions/workers.js.map +1 -0
- package/dist/src/browser/index.js +140 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/json-to-csv-browser.js +225 -0
- package/dist/src/browser/json-to-csv-browser.js.map +1 -0
- package/dist/src/browser/streams.js +340 -0
- package/dist/src/browser/streams.js.map +1 -0
- package/dist/src/browser/workers/csv-parser.worker.js +264 -0
- package/dist/src/browser/workers/csv-parser.worker.js.map +1 -0
- package/dist/src/browser/workers/worker-pool.js +338 -0
- package/dist/src/browser/workers/worker-pool.js.map +1 -0
- package/dist/src/core/delimiter-cache.js +196 -0
- package/dist/src/core/delimiter-cache.js.map +1 -0
- package/dist/src/core/node-optimizations.js +279 -0
- package/dist/src/core/node-optimizations.js.map +1 -0
- package/dist/src/core/plugin-system.js +399 -0
- package/dist/src/core/plugin-system.js.map +1 -0
- package/dist/src/core/transform-hooks.js +348 -0
- package/dist/src/core/transform-hooks.js.map +1 -0
- package/dist/src/engines/fast-path-engine-new.js +262 -0
- package/dist/src/engines/fast-path-engine-new.js.map +1 -0
- package/dist/src/engines/fast-path-engine.js +671 -0
- package/dist/src/engines/fast-path-engine.js.map +1 -0
- package/dist/src/errors.js +18 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/formats/ndjson-parser.js +332 -0
- package/dist/src/formats/ndjson-parser.js.map +1 -0
- package/dist/src/formats/tsv-parser.js +230 -0
- package/dist/src/formats/tsv-parser.js.map +1 -0
- package/dist/src/index-with-plugins.js +259 -0
- package/dist/src/index-with-plugins.js.map +1 -0
- package/dist/src/types/index.js +3 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/utils/bom-utils.js +267 -0
- package/dist/src/utils/bom-utils.js.map +1 -0
- package/dist/src/utils/encoding-support.js +77 -0
- package/dist/src/utils/encoding-support.js.map +1 -0
- package/dist/src/utils/schema-validator.js +609 -0
- package/dist/src/utils/schema-validator.js.map +1 -0
- package/dist/src/utils/transform-loader.js +281 -0
- package/dist/src/utils/transform-loader.js.map +1 -0
- package/dist/src/utils/validators.js +40 -0
- package/dist/src/utils/validators.js.map +1 -0
- package/dist/src/utils/zod-adapter.js +144 -0
- package/dist/src/utils/zod-adapter.js.map +1 -0
- package/{src → dist/src}/web-server/index.js +251 -286
- package/dist/src/web-server/index.js.map +1 -0
- package/dist/src/workers/csv-multithreaded.js +211 -0
- package/dist/src/workers/csv-multithreaded.js.map +1 -0
- package/dist/src/workers/csv-parser.worker.js +179 -0
- package/dist/src/workers/csv-parser.worker.js.map +1 -0
- package/dist/src/workers/worker-pool.js +228 -0
- package/dist/src/workers/worker-pool.js.map +1 -0
- package/dist/stream-csv-to-json.js +665 -0
- package/dist/stream-csv-to-json.js.map +1 -0
- package/dist/stream-json-to-csv.js +389 -0
- package/dist/stream-json-to-csv.js.map +1 -0
- package/examples/advanced/conditional-transformations.ts +2 -2
- package/examples/advanced/performance-optimization.ts +2 -2
- package/examples/cli-advanced-usage.md +2 -0
- package/examples/cli-tool.ts +1 -1
- package/examples/large-dataset-example.ts +2 -2
- package/examples/simple-usage.ts +2 -2
- package/examples/streaming-example.ts +1 -1
- package/index.d.ts +186 -15
- package/package.json +43 -108
- package/plugins.d.ts +37 -0
- package/schema.d.ts +103 -0
- package/src/browser/csv-to-json-browser.ts +233 -3
- package/src/browser/errors-browser.ts +45 -28
- package/src/browser/json-to-csv-browser.ts +81 -5
- package/src/browser/streams.ts +73 -6
- package/src/core/delimiter-cache.ts +21 -11
- package/src/core/plugin-system.ts +343 -155
- package/src/core/transform-hooks.ts +20 -12
- package/src/engines/fast-path-engine.ts +48 -32
- package/src/errors.ts +1 -72
- package/src/formats/ndjson-parser.ts +6 -0
- package/src/formats/tsv-parser.ts +6 -0
- package/src/types/index.ts +21 -1
- package/src/utils/validators.ts +35 -0
- package/src/web-server/index.ts +1 -1
- package/bin/jtcsv.js +0 -2532
- package/csv-to-json.js +0 -711
- package/errors.js +0 -394
- package/examples/advanced/conditional-transformations.js +0 -446
- package/examples/advanced/csv-parser.worker.js +0 -89
- package/examples/advanced/nested-objects-example.js +0 -306
- package/examples/advanced/performance-optimization.js +0 -504
- package/examples/advanced/run-demo-server.js +0 -116
- package/examples/cli-batch-processing.js +0 -38
- package/examples/cli-tool.js +0 -183
- package/examples/error-handling.js +0 -338
- package/examples/express-api.js +0 -164
- package/examples/large-dataset-example.js +0 -182
- package/examples/ndjson-processing.js +0 -434
- package/examples/plugin-excel-exporter.js +0 -406
- package/examples/schema-validation.js +0 -640
- package/examples/simple-usage.js +0 -282
- package/examples/streaming-example.js +0 -418
- package/examples/web-workers-advanced.js +0 -28
- package/index.js +0 -82
- package/json-save.js +0 -255
- package/json-to-csv.js +0 -668
- package/plugins/README.md +0 -91
- package/plugins/express-middleware/README.md +0 -83
- package/plugins/express-middleware/example.js +0 -135
- package/plugins/express-middleware/example.ts +0 -135
- package/plugins/express-middleware/index.d.ts +0 -114
- package/plugins/express-middleware/index.js +0 -512
- package/plugins/express-middleware/index.ts +0 -557
- package/plugins/express-middleware/package.json +0 -52
- package/plugins/fastify-plugin/index.js +0 -404
- package/plugins/fastify-plugin/index.ts +0 -443
- package/plugins/fastify-plugin/package.json +0 -55
- package/plugins/hono/README.md +0 -28
- package/plugins/hono/index.d.ts +0 -12
- package/plugins/hono/index.js +0 -36
- package/plugins/hono/index.ts +0 -226
- package/plugins/hono/package.json +0 -35
- package/plugins/nestjs/README.md +0 -35
- package/plugins/nestjs/index.d.ts +0 -25
- package/plugins/nestjs/index.js +0 -77
- package/plugins/nestjs/index.ts +0 -201
- package/plugins/nestjs/package.json +0 -37
- package/plugins/nextjs-api/README.md +0 -57
- package/plugins/nextjs-api/examples/ConverterComponent.jsx +0 -386
- package/plugins/nextjs-api/examples/ConverterComponent.tsx +0 -386
- package/plugins/nextjs-api/examples/api-convert.js +0 -67
- package/plugins/nextjs-api/examples/api-convert.ts +0 -67
- package/plugins/nextjs-api/index.js +0 -387
- package/plugins/nextjs-api/index.tsx +0 -339
- package/plugins/nextjs-api/package.json +0 -63
- package/plugins/nextjs-api/route.js +0 -370
- package/plugins/nextjs-api/route.ts +0 -370
- package/plugins/nuxt/README.md +0 -24
- package/plugins/nuxt/index.js +0 -21
- package/plugins/nuxt/index.ts +0 -94
- package/plugins/nuxt/package.json +0 -35
- package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
- package/plugins/nuxt/runtime/composables/useJtcsv.ts +0 -100
- package/plugins/nuxt/runtime/plugin.js +0 -6
- package/plugins/nuxt/runtime/plugin.ts +0 -71
- package/plugins/remix/README.md +0 -26
- package/plugins/remix/index.d.ts +0 -16
- package/plugins/remix/index.js +0 -62
- package/plugins/remix/index.ts +0 -260
- package/plugins/remix/package.json +0 -35
- package/plugins/sveltekit/README.md +0 -28
- package/plugins/sveltekit/index.d.ts +0 -17
- package/plugins/sveltekit/index.js +0 -54
- package/plugins/sveltekit/index.ts +0 -301
- package/plugins/sveltekit/package.json +0 -33
- package/plugins/trpc/README.md +0 -25
- package/plugins/trpc/index.d.ts +0 -7
- package/plugins/trpc/index.js +0 -32
- package/plugins/trpc/index.ts +0 -267
- package/plugins/trpc/package.json +0 -34
- package/src/browser/browser-functions.js +0 -219
- package/src/browser/core.js +0 -92
- package/src/browser/csv-to-json-browser.js +0 -722
- package/src/browser/errors-browser.js +0 -212
- package/src/browser/extensions/plugins.js +0 -92
- package/src/browser/extensions/workers.js +0 -39
- package/src/browser/index.js +0 -113
- package/src/browser/json-to-csv-browser.js +0 -319
- package/src/browser/streams.js +0 -403
- package/src/browser/workers/csv-parser.worker.js +0 -377
- package/src/browser/workers/worker-pool.js +0 -527
- package/src/core/delimiter-cache.js +0 -200
- package/src/core/node-optimizations.js +0 -408
- package/src/core/plugin-system.js +0 -494
- package/src/core/transform-hooks.js +0 -350
- package/src/engines/fast-path-engine-new.js +0 -338
- package/src/engines/fast-path-engine.js +0 -844
- package/src/errors.js +0 -26
- package/src/formats/ndjson-parser.js +0 -467
- package/src/formats/tsv-parser.js +0 -339
- package/src/index-with-plugins.js +0 -378
- package/src/utils/bom-utils.js +0 -259
- package/src/utils/encoding-support.js +0 -124
- package/src/utils/schema-validator.js +0 -594
- package/src/utils/transform-loader.js +0 -205
- package/src/utils/zod-adapter.js +0 -170
- package/stream-csv-to-json.js +0 -560
- package/stream-json-to-csv.js +0 -465
package/dist/jtcsv.esm.js
CHANGED
|
@@ -1,1534 +1,1337 @@
|
|
|
1
1
|
// Система ошибок для браузерной версии jtcsv
|
|
2
2
|
// Адаптирована для работы без Node.js специфичных API
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
* Базовый класс ошибки jtcsv
|
|
6
5
|
*/
|
|
7
6
|
class JTCSVError extends Error {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
constructor(message, code = 'JTCSV_ERROR', details = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'JTCSVError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.hint = details.hint;
|
|
13
|
+
this.docs = details.docs;
|
|
14
|
+
this.context = details.context;
|
|
15
|
+
// Сохранение stack trace
|
|
16
|
+
if (Error.captureStackTrace) {
|
|
17
|
+
Error.captureStackTrace(this, JTCSVError);
|
|
18
|
+
}
|
|
17
19
|
}
|
|
18
|
-
}
|
|
19
20
|
}
|
|
20
|
-
|
|
21
21
|
/**
|
|
22
22
|
* Ошибка валидации
|
|
23
23
|
*/
|
|
24
24
|
class ValidationError extends JTCSVError {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
constructor(message, details = {}) {
|
|
26
|
+
super(message, 'VALIDATION_ERROR', details);
|
|
27
|
+
this.name = 'ValidationError';
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
|
-
|
|
31
30
|
/**
|
|
32
31
|
* Ошибка безопасности
|
|
33
32
|
*/
|
|
34
33
|
class SecurityError extends JTCSVError {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
constructor(message, details = {}) {
|
|
35
|
+
super(message, 'SECURITY_ERROR', details);
|
|
36
|
+
this.name = 'SecurityError';
|
|
37
|
+
}
|
|
39
38
|
}
|
|
40
|
-
|
|
41
39
|
/**
|
|
42
40
|
* Ошибка файловой системы (адаптирована для браузера)
|
|
43
41
|
*/
|
|
44
42
|
class FileSystemError extends JTCSVError {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (originalError && originalError.code) {
|
|
52
|
-
this.code = originalError.code;
|
|
43
|
+
constructor(message, originalError, details = {}) {
|
|
44
|
+
super(message, 'FILE_SYSTEM_ERROR', { ...details, originalError });
|
|
45
|
+
this.name = 'FileSystemError';
|
|
46
|
+
if (originalError && originalError.code) {
|
|
47
|
+
this.code = originalError.code;
|
|
48
|
+
}
|
|
53
49
|
}
|
|
54
|
-
}
|
|
55
50
|
}
|
|
56
|
-
|
|
57
51
|
/**
|
|
58
52
|
* Ошибка парсинга
|
|
59
53
|
*/
|
|
60
54
|
class ParsingError extends JTCSVError {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
this.name = 'ParsingError';
|
|
67
|
-
this.lineNumber = lineNumber;
|
|
68
|
-
}
|
|
55
|
+
constructor(message, lineNumber, details = {}) {
|
|
56
|
+
super(message, 'PARSING_ERROR', { ...details, lineNumber });
|
|
57
|
+
this.name = 'ParsingError';
|
|
58
|
+
this.lineNumber = lineNumber;
|
|
59
|
+
}
|
|
69
60
|
}
|
|
70
|
-
|
|
71
61
|
/**
|
|
72
62
|
* Ошибка превышения лимита
|
|
73
63
|
*/
|
|
74
64
|
class LimitError extends JTCSVError {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
this.name = 'LimitError';
|
|
82
|
-
this.limit = limit;
|
|
83
|
-
this.actual = actual;
|
|
84
|
-
}
|
|
65
|
+
constructor(message, limit, actual, details = {}) {
|
|
66
|
+
super(message, 'LIMIT_ERROR', { ...details, limit, actual });
|
|
67
|
+
this.name = 'LimitError';
|
|
68
|
+
this.limit = limit;
|
|
69
|
+
this.actual = actual;
|
|
70
|
+
}
|
|
85
71
|
}
|
|
86
|
-
|
|
87
72
|
/**
|
|
88
73
|
* Ошибка конфигурации
|
|
89
74
|
*/
|
|
90
75
|
class ConfigurationError extends JTCSVError {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
76
|
+
constructor(message, details = {}) {
|
|
77
|
+
super(message, 'CONFIGURATION_ERROR', details);
|
|
78
|
+
this.name = 'ConfigurationError';
|
|
79
|
+
}
|
|
95
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Коды ошибок
|
|
83
|
+
*/
|
|
96
84
|
const ERROR_CODES = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
85
|
+
JTCSV_ERROR: 'JTCSV_ERROR',
|
|
86
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
87
|
+
SECURITY_ERROR: 'SECURITY_ERROR',
|
|
88
|
+
FILE_SYSTEM_ERROR: 'FILE_SYSTEM_ERROR',
|
|
89
|
+
PARSING_ERROR: 'PARSING_ERROR',
|
|
90
|
+
LIMIT_ERROR: 'LIMIT_ERROR',
|
|
91
|
+
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
|
|
92
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
93
|
+
SECURITY_VIOLATION: 'SECURITY_VIOLATION',
|
|
94
|
+
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
|
95
|
+
PARSE_FAILED: 'PARSE_FAILED',
|
|
96
|
+
SIZE_LIMIT: 'SIZE_LIMIT',
|
|
97
|
+
INVALID_CONFIG: 'INVALID_CONFIG',
|
|
98
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
|
111
99
|
};
|
|
112
|
-
|
|
113
100
|
/**
|
|
114
101
|
* Безопасное выполнение функции с обработкой ошибок
|
|
115
|
-
*
|
|
116
|
-
* @param
|
|
117
|
-
* @param
|
|
118
|
-
* @param
|
|
119
|
-
* @returns
|
|
102
|
+
*
|
|
103
|
+
* @param fn - Функция для выполнения
|
|
104
|
+
* @param errorCode - Код ошибки по умолчанию
|
|
105
|
+
* @param errorDetails - Детали ошибки
|
|
106
|
+
* @returns Результат выполнения функции
|
|
120
107
|
*/
|
|
121
108
|
function safeExecute(fn, errorCode = 'UNKNOWN_ERROR', errorDetails = {}) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} catch (error) {
|
|
128
|
-
// Если ошибка уже является JTCSVError, перебросить её
|
|
129
|
-
if (error instanceof JTCSVError) {
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Определить тип ошибки на основе сообщения или кода
|
|
134
|
-
let enhancedError;
|
|
135
|
-
const errorMessage = error.message || String(error);
|
|
136
|
-
if (errorMessage.includes('validation') || errorMessage.includes('Validation')) {
|
|
137
|
-
enhancedError = new ValidationError(errorMessage, {
|
|
138
|
-
...errorDetails,
|
|
139
|
-
originalError: error
|
|
140
|
-
});
|
|
141
|
-
} else if (errorMessage.includes('security') || errorMessage.includes('Security')) {
|
|
142
|
-
enhancedError = new SecurityError(errorMessage, {
|
|
143
|
-
...errorDetails,
|
|
144
|
-
originalError: error
|
|
145
|
-
});
|
|
146
|
-
} else if (errorMessage.includes('parsing') || errorMessage.includes('Parsing')) {
|
|
147
|
-
enhancedError = new ParsingError(errorMessage, null, {
|
|
148
|
-
...errorDetails,
|
|
149
|
-
originalError: error
|
|
150
|
-
});
|
|
151
|
-
} else if (errorMessage.includes('limit') || errorMessage.includes('Limit')) {
|
|
152
|
-
enhancedError = new LimitError(errorMessage, null, null, {
|
|
153
|
-
...errorDetails,
|
|
154
|
-
originalError: error
|
|
155
|
-
});
|
|
156
|
-
} else if (errorMessage.includes('configuration') || errorMessage.includes('Configuration')) {
|
|
157
|
-
enhancedError = new ConfigurationError(errorMessage, {
|
|
158
|
-
...errorDetails,
|
|
159
|
-
originalError: error
|
|
160
|
-
});
|
|
161
|
-
} else if (errorMessage.includes('file') || errorMessage.includes('File')) {
|
|
162
|
-
enhancedError = new FileSystemError(errorMessage, error, errorDetails);
|
|
163
|
-
} else {
|
|
164
|
-
// Общая ошибка
|
|
165
|
-
enhancedError = new JTCSVError(errorMessage, errorCode, {
|
|
166
|
-
...errorDetails,
|
|
167
|
-
originalError: error
|
|
168
|
-
});
|
|
109
|
+
try {
|
|
110
|
+
if (typeof fn === 'function') {
|
|
111
|
+
return fn();
|
|
112
|
+
}
|
|
113
|
+
throw new ValidationError('Function expected');
|
|
169
114
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
115
|
+
catch (error) {
|
|
116
|
+
// Если ошибка уже является JTCSVError, перебросить её
|
|
117
|
+
if (error instanceof JTCSVError) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
// Определить тип ошибки на основе сообщения или кода
|
|
121
|
+
let enhancedError;
|
|
122
|
+
const errorMessage = error.message || String(error);
|
|
123
|
+
if (errorMessage.includes('validation') || errorMessage.includes('Validation')) {
|
|
124
|
+
enhancedError = new ValidationError(errorMessage, { ...errorDetails, originalError: error });
|
|
125
|
+
}
|
|
126
|
+
else if (errorMessage.includes('security') || errorMessage.includes('Security')) {
|
|
127
|
+
enhancedError = new SecurityError(errorMessage, { ...errorDetails, originalError: error });
|
|
128
|
+
}
|
|
129
|
+
else if (errorMessage.includes('parsing') || errorMessage.includes('Parsing')) {
|
|
130
|
+
enhancedError = new ParsingError(errorMessage, undefined, { ...errorDetails, originalError: error });
|
|
131
|
+
}
|
|
132
|
+
else if (errorMessage.includes('limit') || errorMessage.includes('Limit')) {
|
|
133
|
+
enhancedError = new LimitError(errorMessage, null, null, { ...errorDetails, originalError: error });
|
|
134
|
+
}
|
|
135
|
+
else if (errorMessage.includes('configuration') || errorMessage.includes('Configuration')) {
|
|
136
|
+
enhancedError = new ConfigurationError(errorMessage, { ...errorDetails, originalError: error });
|
|
137
|
+
}
|
|
138
|
+
else if (errorMessage.includes('file') || errorMessage.includes('File')) {
|
|
139
|
+
enhancedError = new FileSystemError(errorMessage, error, errorDetails);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Общая ошибка
|
|
143
|
+
enhancedError = new JTCSVError(errorMessage, errorCode, { ...errorDetails, originalError: error });
|
|
144
|
+
}
|
|
145
|
+
// Сохранить оригинальный stack trace если возможно
|
|
146
|
+
if (error.stack) {
|
|
147
|
+
enhancedError.stack = error.stack;
|
|
148
|
+
}
|
|
149
|
+
throw enhancedError;
|
|
174
150
|
}
|
|
175
|
-
throw enhancedError;
|
|
176
|
-
}
|
|
177
151
|
}
|
|
178
|
-
|
|
179
152
|
/**
|
|
180
153
|
* Асинхронная версия safeExecute
|
|
181
154
|
*/
|
|
182
155
|
async function safeExecuteAsync(fn, errorCode = 'UNKNOWN_ERROR', errorDetails = {}) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
156
|
+
try {
|
|
157
|
+
if (typeof fn === 'function') {
|
|
158
|
+
return await fn();
|
|
159
|
+
}
|
|
160
|
+
throw new ValidationError('Function expected');
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
// Если ошибка уже является JTCSVError, перебросить её
|
|
164
|
+
if (error instanceof JTCSVError) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
// Определить тип ошибки
|
|
168
|
+
let enhancedError;
|
|
169
|
+
const errorMessage = error.message || String(error);
|
|
170
|
+
if (errorMessage.includes('validation') || errorMessage.includes('Validation')) {
|
|
171
|
+
enhancedError = new ValidationError(errorMessage, { ...errorDetails, originalError: error });
|
|
172
|
+
}
|
|
173
|
+
else if (errorMessage.includes('security') || errorMessage.includes('Security')) {
|
|
174
|
+
enhancedError = new SecurityError(errorMessage, { ...errorDetails, originalError: error });
|
|
175
|
+
}
|
|
176
|
+
else if (errorMessage.includes('parsing') || errorMessage.includes('Parsing')) {
|
|
177
|
+
enhancedError = new ParsingError(errorMessage, undefined, { ...errorDetails, originalError: error });
|
|
178
|
+
}
|
|
179
|
+
else if (errorMessage.includes('limit') || errorMessage.includes('Limit')) {
|
|
180
|
+
enhancedError = new LimitError(errorMessage, null, null, { ...errorDetails, originalError: error });
|
|
181
|
+
}
|
|
182
|
+
else if (errorMessage.includes('configuration') || errorMessage.includes('Configuration')) {
|
|
183
|
+
enhancedError = new ConfigurationError(errorMessage, { ...errorDetails, originalError: error });
|
|
184
|
+
}
|
|
185
|
+
else if (errorMessage.includes('file') || errorMessage.includes('File')) {
|
|
186
|
+
enhancedError = new FileSystemError(errorMessage, error, errorDetails);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
enhancedError = new JTCSVError(errorMessage, errorCode, { ...errorDetails, originalError: error });
|
|
190
|
+
}
|
|
191
|
+
if (error.stack) {
|
|
192
|
+
enhancedError.stack = error.stack;
|
|
193
|
+
}
|
|
194
|
+
throw enhancedError;
|
|
186
195
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Создать сообщение об ошибке
|
|
199
|
+
*/
|
|
200
|
+
function createErrorMessage(error, includeStack = false) {
|
|
201
|
+
let message = error.message || 'Unknown error';
|
|
190
202
|
if (error instanceof JTCSVError) {
|
|
191
|
-
|
|
203
|
+
message = `[${error.code}] ${message}`;
|
|
204
|
+
if (error instanceof ParsingError && error.lineNumber) {
|
|
205
|
+
message += ` (line ${error.lineNumber})`;
|
|
206
|
+
}
|
|
207
|
+
if (error instanceof LimitError && error.limit && error.actual) {
|
|
208
|
+
message += ` (limit: ${error.limit}, actual: ${error.actual})`;
|
|
209
|
+
}
|
|
210
|
+
if (error.hint) {
|
|
211
|
+
message += `\nHint: ${error.hint}`;
|
|
212
|
+
}
|
|
213
|
+
if (error.docs) {
|
|
214
|
+
message += `\nDocs: ${error.docs}`;
|
|
215
|
+
}
|
|
192
216
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
...errorDetails,
|
|
215
|
-
originalError: error
|
|
216
|
-
});
|
|
217
|
-
} else if (errorMessage.includes('configuration') || errorMessage.includes('Configuration')) {
|
|
218
|
-
enhancedError = new ConfigurationError(errorMessage, {
|
|
219
|
-
...errorDetails,
|
|
220
|
-
originalError: error
|
|
221
|
-
});
|
|
222
|
-
} else if (errorMessage.includes('file') || errorMessage.includes('File')) {
|
|
223
|
-
enhancedError = new FileSystemError(errorMessage, error, errorDetails);
|
|
224
|
-
} else {
|
|
225
|
-
enhancedError = new JTCSVError(errorMessage, errorCode, {
|
|
226
|
-
...errorDetails,
|
|
227
|
-
originalError: error
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
if (error.stack) {
|
|
231
|
-
enhancedError.stack = error.stack;
|
|
232
|
-
}
|
|
233
|
-
throw enhancedError;
|
|
234
|
-
}
|
|
217
|
+
if (includeStack && error.stack) {
|
|
218
|
+
message += `\n${error.stack}`;
|
|
219
|
+
}
|
|
220
|
+
return message;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Обработка ошибки
|
|
224
|
+
*/
|
|
225
|
+
function handleError(error, options = {}) {
|
|
226
|
+
const { log = true, throw: shouldThrow = false, format = true } = options;
|
|
227
|
+
const message = format ? createErrorMessage(error) : error.message;
|
|
228
|
+
if (log) {
|
|
229
|
+
console.error(`[jtcsv] ${message}`);
|
|
230
|
+
if (error instanceof JTCSVError && error.details) {
|
|
231
|
+
console.error('Error details:', error.details);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (shouldThrow) {
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
return message;
|
|
235
238
|
}
|
|
236
|
-
|
|
237
239
|
// Экспорт для Node.js совместимости
|
|
238
240
|
if (typeof module !== 'undefined' && module.exports) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
241
|
+
module.exports = {
|
|
242
|
+
JTCSVError,
|
|
243
|
+
ValidationError,
|
|
244
|
+
SecurityError,
|
|
245
|
+
FileSystemError,
|
|
246
|
+
ParsingError,
|
|
247
|
+
LimitError,
|
|
248
|
+
ConfigurationError,
|
|
249
|
+
ERROR_CODES,
|
|
250
|
+
safeExecute,
|
|
251
|
+
safeExecuteAsync,
|
|
252
|
+
createErrorMessage,
|
|
253
|
+
handleError
|
|
254
|
+
};
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
// Браузерная версия JSON to CSV конвертера
|
|
254
258
|
// Адаптирована для работы в браузере без Node.js API
|
|
255
|
-
|
|
256
|
-
|
|
257
259
|
/**
|
|
258
260
|
* Валидация входных данных и опций
|
|
259
261
|
* @private
|
|
260
262
|
*/
|
|
261
263
|
function validateInput(data, options) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Validate options
|
|
268
|
-
if (options && typeof options !== 'object') {
|
|
269
|
-
throw new ConfigurationError('Options must be an object');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Validate delimiter
|
|
273
|
-
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
274
|
-
throw new ConfigurationError('Delimiter must be a string');
|
|
275
|
-
}
|
|
276
|
-
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
277
|
-
throw new ConfigurationError('Delimiter must be a single character');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Validate renameMap
|
|
281
|
-
if (options?.renameMap && typeof options.renameMap !== 'object') {
|
|
282
|
-
throw new ConfigurationError('renameMap must be an object');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Validate maxRecords
|
|
286
|
-
if (options && options.maxRecords !== undefined) {
|
|
287
|
-
if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
|
|
288
|
-
throw new ConfigurationError('maxRecords must be a positive number');
|
|
264
|
+
// Validate data
|
|
265
|
+
if (!Array.isArray(data)) {
|
|
266
|
+
throw new ValidationError('Input data must be an array');
|
|
289
267
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (options?.preventCsvInjection !== undefined && typeof options.preventCsvInjection !== 'boolean') {
|
|
294
|
-
throw new ConfigurationError('preventCsvInjection must be a boolean');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Validate rfc4180Compliant
|
|
298
|
-
if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
|
|
299
|
-
throw new ConfigurationError('rfc4180Compliant must be a boolean');
|
|
300
|
-
}
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Конвертирует JSON данные в CSV формат
|
|
306
|
-
*
|
|
307
|
-
* @param {Array<Object>} data - Массив объектов для конвертации в CSV
|
|
308
|
-
* @param {Object} [options] - Опции конфигурации
|
|
309
|
-
* @param {string} [options.delimiter=';'] - CSV разделитель
|
|
310
|
-
* @param {boolean} [options.includeHeaders=true] - Включать ли заголовки
|
|
311
|
-
* @param {Object} [options.renameMap={}] - Маппинг переименования заголовков
|
|
312
|
-
* @param {Object} [options.template={}] - Шаблон для порядка колонок
|
|
313
|
-
* @param {number} [options.maxRecords] - Максимальное количество записей
|
|
314
|
-
* @param {boolean} [options.preventCsvInjection=true] - Защита от CSV инъекций
|
|
315
|
-
* @param {boolean} [options.rfc4180Compliant=true] - Соответствие RFC 4180
|
|
316
|
-
* @returns {string} CSV строка
|
|
317
|
-
*/
|
|
318
|
-
function jsonToCsv(data, options = {}) {
|
|
319
|
-
return safeExecute(() => {
|
|
320
|
-
// Валидация входных данных
|
|
321
|
-
validateInput(data, options);
|
|
322
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
323
|
-
const {
|
|
324
|
-
delimiter = ';',
|
|
325
|
-
includeHeaders = true,
|
|
326
|
-
renameMap = {},
|
|
327
|
-
template = {},
|
|
328
|
-
maxRecords,
|
|
329
|
-
preventCsvInjection = true,
|
|
330
|
-
rfc4180Compliant = true
|
|
331
|
-
} = opts;
|
|
332
|
-
|
|
333
|
-
// Обработка пустых данных
|
|
334
|
-
if (data.length === 0) {
|
|
335
|
-
return '';
|
|
268
|
+
// Validate options
|
|
269
|
+
if (options && typeof options !== 'object') {
|
|
270
|
+
throw new ConfigurationError('Options must be an object');
|
|
336
271
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.warn('⚠️ Warning: Processing >1M records in memory may be slow.\n' + '💡 Consider processing data in batches or using Web Workers for large files.\n' + '📊 Current size: ' + data.length.toLocaleString() + ' records');
|
|
272
|
+
// Validate delimiter
|
|
273
|
+
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
274
|
+
throw new ConfigurationError('Delimiter must be a string');
|
|
341
275
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (maxRecords && data.length > maxRecords) {
|
|
345
|
-
throw new LimitError(`Data size exceeds maximum limit of ${maxRecords} records`, maxRecords, data.length);
|
|
276
|
+
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
277
|
+
throw new ConfigurationError('Delimiter must be a single character');
|
|
346
278
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
data.forEach(item => {
|
|
351
|
-
if (!item || typeof item !== 'object') {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
Object.keys(item).forEach(key => allKeys.add(key));
|
|
355
|
-
});
|
|
356
|
-
const originalKeys = Array.from(allKeys);
|
|
357
|
-
|
|
358
|
-
// Применение rename map для создания заголовков
|
|
359
|
-
const headers = originalKeys.map(key => renameMap[key] || key);
|
|
360
|
-
|
|
361
|
-
// Создание обратного маппинга
|
|
362
|
-
const reverseRenameMap = {};
|
|
363
|
-
originalKeys.forEach((key, index) => {
|
|
364
|
-
reverseRenameMap[headers[index]] = key;
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Применение порядка из шаблона
|
|
368
|
-
let finalHeaders = headers;
|
|
369
|
-
if (Object.keys(template).length > 0) {
|
|
370
|
-
const templateHeaders = Object.keys(template).map(key => renameMap[key] || key);
|
|
371
|
-
const extraHeaders = headers.filter(h => !templateHeaders.includes(h));
|
|
372
|
-
finalHeaders = [...templateHeaders, ...extraHeaders];
|
|
279
|
+
// Validate renameMap
|
|
280
|
+
if (options?.renameMap && typeof options.renameMap !== 'object') {
|
|
281
|
+
throw new ConfigurationError('renameMap must be an object');
|
|
373
282
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const escapeValue = value => {
|
|
380
|
-
if (value === null || value === undefined || value === '') {
|
|
381
|
-
return '';
|
|
382
|
-
}
|
|
383
|
-
const stringValue = String(value);
|
|
384
|
-
|
|
385
|
-
// Защита от CSV инъекций
|
|
386
|
-
let escapedValue = stringValue;
|
|
387
|
-
if (preventCsvInjection) {
|
|
388
|
-
// Dangerous prefixes: =, +, -, @, tab (\t), carriage return (\r)
|
|
389
|
-
if (/^[=+\-@\t\r]/.test(stringValue)) {
|
|
390
|
-
escapedValue = "'" + stringValue;
|
|
391
|
-
}
|
|
392
|
-
// Unicode Bidi override characters
|
|
393
|
-
const bidiChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
|
|
394
|
-
for (const bidi of bidiChars) {
|
|
395
|
-
if (stringValue.includes(bidi)) {
|
|
396
|
-
escapedValue = escapedValue.replace(new RegExp(bidi, 'g'), '');
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Соответствие RFC 4180
|
|
402
|
-
const needsQuoting = rfc4180Compliant ? escapedValue.includes(delimiter) || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r') : escapedValue.includes(delimiter) || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r');
|
|
403
|
-
if (needsQuoting) {
|
|
404
|
-
return `"${escapedValue.replace(/"/g, '""')}"`;
|
|
405
|
-
}
|
|
406
|
-
return escapedValue;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
// Построение CSV строк
|
|
410
|
-
const rows = [];
|
|
411
|
-
|
|
412
|
-
// Добавление заголовков
|
|
413
|
-
if (includeHeaders && finalHeaders.length > 0) {
|
|
414
|
-
rows.push(finalHeaders.join(delimiter));
|
|
283
|
+
// Validate maxRecords
|
|
284
|
+
if (options && options.maxRecords !== undefined) {
|
|
285
|
+
if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
|
|
286
|
+
throw new ConfigurationError('maxRecords must be a positive number');
|
|
287
|
+
}
|
|
415
288
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (!item || typeof item !== 'object') {
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
const row = finalHeaders.map(header => {
|
|
423
|
-
const originalKey = reverseRenameMap[header] || header;
|
|
424
|
-
const value = item[originalKey];
|
|
425
|
-
return escapeValue(value);
|
|
426
|
-
}).join(delimiter);
|
|
427
|
-
rows.push(row);
|
|
289
|
+
// Validate preventCsvInjection
|
|
290
|
+
if (options?.preventCsvInjection !== undefined && typeof options.preventCsvInjection !== 'boolean') {
|
|
291
|
+
throw new ConfigurationError('preventCsvInjection must be a boolean');
|
|
428
292
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
293
|
+
// Validate rfc4180Compliant
|
|
294
|
+
if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
|
|
295
|
+
throw new ConfigurationError('rfc4180Compliant must be a boolean');
|
|
296
|
+
}
|
|
297
|
+
if (options?.normalizeQuotes !== undefined && typeof options.normalizeQuotes !== 'boolean') {
|
|
298
|
+
throw new ConfigurationError('normalizeQuotes must be a boolean');
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
436
301
|
}
|
|
437
|
-
|
|
438
302
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
* @returns {string} Развернутое строковое значение
|
|
446
|
-
*/
|
|
447
|
-
function deepUnwrap(value, depth = 0, maxDepth = 5, visited = new Set()) {
|
|
448
|
-
// Проверка глубины
|
|
449
|
-
if (depth >= maxDepth) {
|
|
450
|
-
return '[Too Deep]';
|
|
451
|
-
}
|
|
452
|
-
if (value === null || value === undefined) {
|
|
453
|
-
return '';
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Обработка циклических ссылок
|
|
457
|
-
if (typeof value === 'object') {
|
|
458
|
-
if (visited.has(value)) {
|
|
459
|
-
return '[Circular Reference]';
|
|
303
|
+
* Экранирование CSV значений для предотвращения инъекций
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
function escapeCsvValue(value, preventInjection = true) {
|
|
307
|
+
if (value === null || value === undefined) {
|
|
308
|
+
return '';
|
|
460
309
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
310
|
+
const str = String(value);
|
|
311
|
+
const isPotentialFormula = (input) => {
|
|
312
|
+
let idx = 0;
|
|
313
|
+
while (idx < input.length) {
|
|
314
|
+
const code = input.charCodeAt(idx);
|
|
315
|
+
if (code === 32 || code === 9 || code === 10 || code === 13 || code === 0xfeff) {
|
|
316
|
+
idx++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
if (idx < input.length && (input[idx] === '"' || input[idx] === "'")) {
|
|
322
|
+
idx++;
|
|
323
|
+
while (idx < input.length) {
|
|
324
|
+
const code = input.charCodeAt(idx);
|
|
325
|
+
if (code === 32 || code === 9) {
|
|
326
|
+
idx++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (idx >= input.length) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
const char = input[idx];
|
|
336
|
+
return char === '=' || char === '+' || char === '-' || char === '@';
|
|
337
|
+
};
|
|
338
|
+
// Экранирование формул для предотвращения CSV инъекций
|
|
339
|
+
if (preventInjection && isPotentialFormula(str)) {
|
|
340
|
+
return "'" + str;
|
|
478
341
|
}
|
|
479
|
-
|
|
480
|
-
|
|
342
|
+
// Экранирование кавычек и переносов строк
|
|
343
|
+
if (str.includes('"') || str.includes('\n') || str.includes('\r') || str.includes(',')) {
|
|
344
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
481
345
|
}
|
|
482
|
-
|
|
483
|
-
// Сериализация сложных объектов
|
|
484
|
-
try {
|
|
485
|
-
return JSON.stringify(value);
|
|
486
|
-
} catch (error) {
|
|
487
|
-
if (error.message.includes('circular') || error.message.includes('Converting circular')) {
|
|
488
|
-
return '[Circular Reference]';
|
|
489
|
-
}
|
|
490
|
-
return '[Unstringifiable Object]';
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Примитивные значения
|
|
495
|
-
return String(value);
|
|
346
|
+
return str;
|
|
496
347
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const processed = {};
|
|
513
|
-
for (const key in item) {
|
|
514
|
-
if (Object.prototype.hasOwnProperty.call(item, key)) {
|
|
515
|
-
const value = item[key];
|
|
516
|
-
if (value && typeof value === 'object') {
|
|
517
|
-
processed[key] = deepUnwrap(value);
|
|
518
|
-
} else {
|
|
519
|
-
processed[key] = value;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return processed;
|
|
524
|
-
});
|
|
348
|
+
function normalizeQuotesInField$2(value) {
|
|
349
|
+
// Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
|
|
350
|
+
// Проверяем, выглядит ли значение как JSON (объект или массив)
|
|
351
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
352
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
353
|
+
return value; // Возвращаем как есть для JSON
|
|
354
|
+
}
|
|
355
|
+
let normalized = value.replace(/"{2,}/g, '"');
|
|
356
|
+
// Убираем правило, которое ломает JSON: не заменяем "," на ","
|
|
357
|
+
// normalized = normalized.replace(/"\s*,\s*"/g, ',');
|
|
358
|
+
normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
|
|
359
|
+
if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
360
|
+
normalized = normalized.slice(1, -1);
|
|
361
|
+
}
|
|
362
|
+
return normalized;
|
|
525
363
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
364
|
+
function normalizePhoneValue$2(value) {
|
|
365
|
+
const trimmed = value.trim();
|
|
366
|
+
if (trimmed === '') {
|
|
367
|
+
return trimmed;
|
|
368
|
+
}
|
|
369
|
+
return trimmed.replace(/["'\\]/g, '');
|
|
370
|
+
}
|
|
371
|
+
function normalizeValueForCsv$1(value, key, normalizeQuotes) {
|
|
372
|
+
if (!normalizeQuotes || typeof value !== 'string') {
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
const base = normalizeQuotesInField$2(value);
|
|
376
|
+
if (!key) {
|
|
377
|
+
return base;
|
|
378
|
+
}
|
|
379
|
+
const phoneKeys = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
|
|
380
|
+
if (phoneKeys.has(String(key).toLowerCase())) {
|
|
381
|
+
return normalizePhoneValue$2(base);
|
|
382
|
+
}
|
|
383
|
+
return base;
|
|
534
384
|
}
|
|
535
|
-
|
|
536
|
-
// Браузерная версия CSV to JSON конвертера
|
|
537
|
-
// Адаптирована для работы в браузере без Node.js API
|
|
538
|
-
|
|
539
|
-
|
|
540
385
|
/**
|
|
541
|
-
*
|
|
386
|
+
* Извлечение всех уникальных ключей из массива объектов
|
|
542
387
|
* @private
|
|
543
388
|
*/
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
throw new ConfigurationError('Delimiter must be a string');
|
|
553
|
-
}
|
|
554
|
-
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
555
|
-
throw new ConfigurationError('Delimiter must be a single character');
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Validate autoDetect
|
|
559
|
-
if (options?.autoDetect !== undefined && typeof options.autoDetect !== 'boolean') {
|
|
560
|
-
throw new ConfigurationError('autoDetect must be a boolean');
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Validate candidates
|
|
564
|
-
if (options?.candidates && !Array.isArray(options.candidates)) {
|
|
565
|
-
throw new ConfigurationError('candidates must be an array');
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Validate maxRows
|
|
569
|
-
if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
|
|
570
|
-
throw new ConfigurationError('maxRows must be a positive number');
|
|
571
|
-
}
|
|
572
|
-
if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
|
|
573
|
-
throw new ConfigurationError('warnExtraFields must be a boolean');
|
|
574
|
-
}
|
|
575
|
-
return true;
|
|
389
|
+
function extractAllKeys(data) {
|
|
390
|
+
const keys = new Set();
|
|
391
|
+
for (const item of data) {
|
|
392
|
+
if (item && typeof item === 'object') {
|
|
393
|
+
Object.keys(item).forEach(key => keys.add(key));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return Array.from(keys);
|
|
576
397
|
}
|
|
577
|
-
|
|
578
398
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
399
|
+
* Конвертация массива объектов в CSV строку
|
|
400
|
+
*
|
|
401
|
+
* @param data - Массив объектов для конвертации
|
|
402
|
+
* @param options - Опции конвертации
|
|
403
|
+
* @returns CSV строка
|
|
581
404
|
*/
|
|
582
|
-
function
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
405
|
+
function jsonToCsv$1(data, options = {}) {
|
|
406
|
+
return safeExecute(() => {
|
|
407
|
+
validateInput(data, options);
|
|
408
|
+
if (data.length === 0) {
|
|
409
|
+
return '';
|
|
410
|
+
}
|
|
411
|
+
// Настройки по умолчанию
|
|
412
|
+
const delimiter = options.delimiter || ';';
|
|
413
|
+
const includeHeaders = options.includeHeaders !== false;
|
|
414
|
+
const maxRecords = options.maxRecords || data.length;
|
|
415
|
+
const preventInjection = options.preventCsvInjection !== false;
|
|
416
|
+
const rfc4180Compliant = options.rfc4180Compliant !== false;
|
|
417
|
+
const normalizeQuotes = options.normalizeQuotes !== false;
|
|
418
|
+
// Ограничение количества записей
|
|
419
|
+
const limitedData = data.slice(0, maxRecords);
|
|
420
|
+
// Извлечение всех ключей
|
|
421
|
+
const allKeys = extractAllKeys(limitedData);
|
|
422
|
+
// Применение renameMap если есть
|
|
423
|
+
const renameMap = options.renameMap || {};
|
|
424
|
+
const finalKeys = allKeys.map(key => renameMap[key] || key);
|
|
425
|
+
// Создание CSV строки
|
|
426
|
+
const lines = [];
|
|
427
|
+
// Заголовки
|
|
428
|
+
if (includeHeaders) {
|
|
429
|
+
const headerLine = finalKeys.map(key => escapeCsvValue(key, preventInjection)).join(delimiter);
|
|
430
|
+
lines.push(headerLine);
|
|
431
|
+
}
|
|
432
|
+
// Данные
|
|
433
|
+
for (const item of limitedData) {
|
|
434
|
+
const rowValues = allKeys.map(key => {
|
|
435
|
+
const value = item?.[key];
|
|
436
|
+
const normalized = normalizeValueForCsv$1(value, key, normalizeQuotes);
|
|
437
|
+
return escapeCsvValue(normalized, preventInjection);
|
|
438
|
+
});
|
|
439
|
+
lines.push(rowValues.join(delimiter));
|
|
440
|
+
}
|
|
441
|
+
// RFC 4180 compliance: CRLF line endings
|
|
442
|
+
if (rfc4180Compliant) {
|
|
443
|
+
return lines.join('\r\n');
|
|
444
|
+
}
|
|
445
|
+
return lines.join('\n');
|
|
446
|
+
});
|
|
588
447
|
}
|
|
589
|
-
|
|
590
448
|
/**
|
|
591
|
-
*
|
|
592
|
-
* @private
|
|
449
|
+
* Асинхронная версия jsonToCsv
|
|
593
450
|
*/
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
let currentField = '';
|
|
597
|
-
let insideQuotes = false;
|
|
598
|
-
let escapeNext = false;
|
|
599
|
-
for (let i = 0; i < line.length; i++) {
|
|
600
|
-
const char = line[i];
|
|
601
|
-
if (escapeNext) {
|
|
602
|
-
currentField += char;
|
|
603
|
-
escapeNext = false;
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
if (char === '\\') {
|
|
607
|
-
if (i + 1 === line.length) {
|
|
608
|
-
// Обратный слеш в конце строки
|
|
609
|
-
currentField += char;
|
|
610
|
-
} else if (line[i + 1] === '\\') {
|
|
611
|
-
// Двойной обратный слеш
|
|
612
|
-
currentField += char;
|
|
613
|
-
i++; // Пропустить следующий слеш
|
|
614
|
-
} else {
|
|
615
|
-
// Экранирование следующего символа
|
|
616
|
-
escapeNext = true;
|
|
617
|
-
}
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
if (char === '"') {
|
|
621
|
-
if (insideQuotes) {
|
|
622
|
-
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
623
|
-
// Экранированная кавычка внутри кавычек
|
|
624
|
-
currentField += '"';
|
|
625
|
-
i++; // Пропустить следующую кавычку
|
|
626
|
-
|
|
627
|
-
// Проверка конца поля
|
|
628
|
-
let isEndOfField = false;
|
|
629
|
-
let j = i + 1;
|
|
630
|
-
while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
|
|
631
|
-
j++;
|
|
632
|
-
}
|
|
633
|
-
if (j === line.length || line[j] === delimiter) {
|
|
634
|
-
isEndOfField = true;
|
|
635
|
-
}
|
|
636
|
-
if (isEndOfField) {
|
|
637
|
-
insideQuotes = false;
|
|
638
|
-
}
|
|
639
|
-
} else {
|
|
640
|
-
// Проверка конца поля
|
|
641
|
-
let isEndOfField = false;
|
|
642
|
-
let j = i + 1;
|
|
643
|
-
while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
|
|
644
|
-
j++;
|
|
645
|
-
}
|
|
646
|
-
if (j === line.length || line[j] === delimiter) {
|
|
647
|
-
isEndOfField = true;
|
|
648
|
-
}
|
|
649
|
-
if (isEndOfField) {
|
|
650
|
-
insideQuotes = false;
|
|
651
|
-
} else {
|
|
652
|
-
currentField += '"';
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
} else {
|
|
656
|
-
// Начало поля в кавычках
|
|
657
|
-
insideQuotes = true;
|
|
658
|
-
}
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
if (!insideQuotes && char === delimiter) {
|
|
662
|
-
// Конец поля
|
|
663
|
-
fields.push(currentField);
|
|
664
|
-
currentField = '';
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
currentField += char;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Обработка незавершенного экранирования
|
|
671
|
-
if (escapeNext) {
|
|
672
|
-
currentField += '\\';
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Добавление последнего поля
|
|
676
|
-
fields.push(currentField);
|
|
677
|
-
|
|
678
|
-
// Проверка незакрытых кавычек
|
|
679
|
-
if (insideQuotes) {
|
|
680
|
-
throw new ParsingError('Unclosed quotes in CSV', lineNumber);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Валидация количества полей
|
|
684
|
-
if (fields.length === 0) {
|
|
685
|
-
throw new ParsingError('No fields found', lineNumber);
|
|
686
|
-
}
|
|
687
|
-
return fields;
|
|
451
|
+
async function jsonToCsvAsync$1(data, options = {}) {
|
|
452
|
+
return jsonToCsv$1(data, options);
|
|
688
453
|
}
|
|
689
|
-
|
|
690
454
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
455
|
+
* Создает итератор для потоковой конвертации JSON в CSV
|
|
456
|
+
*
|
|
457
|
+
* @param data - Массив объектов или async итератор
|
|
458
|
+
* @param options - Опции конвертации
|
|
459
|
+
* @returns AsyncIterator с CSV чанками
|
|
693
460
|
*/
|
|
694
|
-
function
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
// Парсинг булевых значений
|
|
727
|
-
if (parseBooleans) {
|
|
728
|
-
const lowerValue = result.toLowerCase();
|
|
729
|
-
if (lowerValue === 'true') {
|
|
730
|
-
return true;
|
|
461
|
+
async function* jsonToCsvIterator(data, options = {}) {
|
|
462
|
+
validateInput(Array.isArray(data) ? data : [], options);
|
|
463
|
+
const delimiter = options.delimiter || ';';
|
|
464
|
+
const includeHeaders = options.includeHeaders !== false;
|
|
465
|
+
const preventInjection = options.preventCsvInjection !== false;
|
|
466
|
+
const rfc4180Compliant = options.rfc4180Compliant !== false;
|
|
467
|
+
const normalizeQuotes = options.normalizeQuotes !== false;
|
|
468
|
+
let allKeys = [];
|
|
469
|
+
let renameMap = {};
|
|
470
|
+
// Если данные - массив, обрабатываем как массив
|
|
471
|
+
if (Array.isArray(data)) {
|
|
472
|
+
if (data.length === 0) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
allKeys = extractAllKeys(data);
|
|
476
|
+
renameMap = options.renameMap || {};
|
|
477
|
+
const finalKeys = allKeys.map(key => renameMap[key] || key);
|
|
478
|
+
// Заголовки
|
|
479
|
+
if (includeHeaders) {
|
|
480
|
+
const headerLine = finalKeys.map(key => escapeCsvValue(key, preventInjection)).join(delimiter);
|
|
481
|
+
yield headerLine + (rfc4180Compliant ? '\r\n' : '\n');
|
|
482
|
+
}
|
|
483
|
+
// Данные
|
|
484
|
+
for (const item of data) {
|
|
485
|
+
const rowValues = allKeys.map(key => {
|
|
486
|
+
const value = item?.[key];
|
|
487
|
+
const normalized = normalizeValueForCsv$1(value, key, normalizeQuotes);
|
|
488
|
+
return escapeCsvValue(normalized, preventInjection);
|
|
489
|
+
});
|
|
490
|
+
yield rowValues.join(delimiter) + (rfc4180Compliant ? '\r\n' : '\n');
|
|
491
|
+
}
|
|
731
492
|
}
|
|
732
|
-
|
|
733
|
-
|
|
493
|
+
else {
|
|
494
|
+
// Для async итератора нужна другая логика
|
|
495
|
+
throw new ValidationError('Async iterators not yet implemented in browser version');
|
|
734
496
|
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Пустые строки как null
|
|
738
|
-
if (result === '') {
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
return result;
|
|
742
|
-
}
|
|
743
|
-
function isSimpleCsv(csv) {
|
|
744
|
-
return csv.indexOf('"') === -1 && csv.indexOf('\\') === -1;
|
|
745
|
-
}
|
|
746
|
-
function parseSimpleCsv(csv, delimiter, options) {
|
|
747
|
-
const {
|
|
748
|
-
hasHeaders = true,
|
|
749
|
-
renameMap = {},
|
|
750
|
-
trim = true,
|
|
751
|
-
parseNumbers = false,
|
|
752
|
-
parseBooleans = false,
|
|
753
|
-
maxRows
|
|
754
|
-
} = options;
|
|
755
|
-
const result = [];
|
|
756
|
-
let headers = null;
|
|
757
|
-
let fieldStart = 0;
|
|
758
|
-
let currentRow = [];
|
|
759
|
-
let rowHasData = false;
|
|
760
|
-
let rowCount = 0;
|
|
761
|
-
const finalizeRow = fields => {
|
|
762
|
-
if (fields.length === 1 && fields[0].trim() === '') {
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
if (!headers) {
|
|
766
|
-
if (hasHeaders) {
|
|
767
|
-
headers = fields.map(header => {
|
|
768
|
-
const trimmed = trim ? header.trim() : header;
|
|
769
|
-
return renameMap[trimmed] || trimmed;
|
|
770
|
-
});
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
headers = fields.map((_, index) => `column${index + 1}`);
|
|
774
|
-
}
|
|
775
|
-
rowCount++;
|
|
776
|
-
if (maxRows && rowCount > maxRows) {
|
|
777
|
-
throw new LimitError(`CSV size exceeds maximum limit of ${maxRows} rows`, maxRows, rowCount);
|
|
778
|
-
}
|
|
779
|
-
const row = {};
|
|
780
|
-
const fieldCount = Math.min(fields.length, headers.length);
|
|
781
|
-
for (let i = 0; i < fieldCount; i++) {
|
|
782
|
-
row[headers[i]] = parseCsvValue(fields[i], {
|
|
783
|
-
trim,
|
|
784
|
-
parseNumbers,
|
|
785
|
-
parseBooleans
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
result.push(row);
|
|
789
|
-
};
|
|
790
|
-
let i = 0;
|
|
791
|
-
while (i <= csv.length) {
|
|
792
|
-
const char = i < csv.length ? csv[i] : '\n';
|
|
793
|
-
if (char !== '\r' && char !== '\n' && char !== ' ' && char !== '\t') {
|
|
794
|
-
rowHasData = true;
|
|
795
|
-
}
|
|
796
|
-
if (char === delimiter || char === '\n' || char === '\r' || i === csv.length) {
|
|
797
|
-
const field = csv.slice(fieldStart, i);
|
|
798
|
-
currentRow.push(field);
|
|
799
|
-
if (char === '\n' || char === '\r' || i === csv.length) {
|
|
800
|
-
if (rowHasData || currentRow.length > 1) {
|
|
801
|
-
finalizeRow(currentRow);
|
|
802
|
-
}
|
|
803
|
-
currentRow = [];
|
|
804
|
-
rowHasData = false;
|
|
805
|
-
}
|
|
806
|
-
if (char === '\r' && csv[i + 1] === '\n') {
|
|
807
|
-
i++;
|
|
808
|
-
}
|
|
809
|
-
fieldStart = i + 1;
|
|
810
|
-
}
|
|
811
|
-
i++;
|
|
812
|
-
}
|
|
813
|
-
return result;
|
|
814
497
|
}
|
|
815
|
-
|
|
816
498
|
/**
|
|
817
|
-
*
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
*
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
// Быстрый подсчёт вхождений кандидатов за один проход
|
|
836
|
-
const counts = {};
|
|
837
|
-
const candidateSet = new Set(candidates);
|
|
838
|
-
for (let i = 0; i < firstLine.length; i++) {
|
|
839
|
-
const char = firstLine[i];
|
|
840
|
-
if (candidateSet.has(char)) {
|
|
841
|
-
counts[char] = (counts[char] || 0) + 1;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
// Убедимся, что все кандидаты присутствуют в counts (даже с нулём)
|
|
845
|
-
for (const delim of candidates) {
|
|
846
|
-
if (!(delim in counts)) {
|
|
847
|
-
counts[delim] = 0;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Поиск разделителя с максимальным количеством
|
|
852
|
-
let maxCount = -1;
|
|
853
|
-
let detectedDelimiter = ';'; // значение по умолчанию
|
|
854
|
-
const maxDelimiters = [];
|
|
855
|
-
for (const [delim, count] of Object.entries(counts)) {
|
|
856
|
-
if (count > maxCount) {
|
|
857
|
-
maxCount = count;
|
|
858
|
-
maxDelimiters.length = 0;
|
|
859
|
-
maxDelimiters.push(delim);
|
|
860
|
-
} else if (count === maxCount) {
|
|
861
|
-
maxDelimiters.push(delim);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// Если разделитель не найден или есть ничья, возвращаем стандартный
|
|
866
|
-
if (maxCount === 0 || maxDelimiters.length > 1) {
|
|
867
|
-
detectedDelimiter = ';';
|
|
868
|
-
} else {
|
|
869
|
-
detectedDelimiter = maxDelimiters[0];
|
|
870
|
-
}
|
|
871
|
-
return detectedDelimiter;
|
|
499
|
+
* Асинхронная версия jsonToCsvIterator (псевдоним)
|
|
500
|
+
*/
|
|
501
|
+
const jsonToCsvIteratorAsync = jsonToCsvIterator;
|
|
502
|
+
/**
|
|
503
|
+
* Безопасная конвертация с обработкой ошибок
|
|
504
|
+
*
|
|
505
|
+
* @param data - Массив объектов
|
|
506
|
+
* @param options - Опции конвертации
|
|
507
|
+
* @returns CSV строка или null при ошибке
|
|
508
|
+
*/
|
|
509
|
+
function jsonToCsvSafe(data, options = {}) {
|
|
510
|
+
try {
|
|
511
|
+
return jsonToCsv$1(data, options);
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
console.error('JSON to CSV conversion error:', error);
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
872
517
|
}
|
|
873
|
-
|
|
874
518
|
/**
|
|
875
|
-
*
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
* @param {boolean} [options.autoDetect=true] - Автоопределение разделителя
|
|
881
|
-
* @param {Array} [options.candidates=[';', ',', '\t', '|']] - Кандидаты для автоопределения
|
|
882
|
-
* @param {boolean} [options.hasHeaders=true] - Есть ли заголовки в CSV
|
|
883
|
-
* @param {Object} [options.renameMap={}] - Маппинг переименования заголовков
|
|
884
|
-
* @param {boolean} [options.trim=true] - Обрезать пробелы
|
|
885
|
-
* @param {boolean} [options.parseNumbers=false] - Парсить числовые значения
|
|
886
|
-
* @param {boolean} [options.parseBooleans=false] - Парсить булевы значения
|
|
887
|
-
* @param {number} [options.maxRows] - Максимальное количество строк
|
|
888
|
-
* @returns {Array<Object>} JSON массив
|
|
889
|
-
*/
|
|
890
|
-
function csvToJson(csv, options = {}) {
|
|
891
|
-
return safeExecute(() => {
|
|
892
|
-
// Валидация ввода
|
|
893
|
-
validateCsvInput(csv, options);
|
|
894
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
895
|
-
const {
|
|
896
|
-
delimiter,
|
|
897
|
-
autoDetect = true,
|
|
898
|
-
candidates = [';', ',', '\t', '|'],
|
|
899
|
-
hasHeaders = true,
|
|
900
|
-
renameMap = {},
|
|
901
|
-
trim = true,
|
|
902
|
-
parseNumbers = false,
|
|
903
|
-
parseBooleans = false,
|
|
904
|
-
maxRows,
|
|
905
|
-
warnExtraFields = true
|
|
906
|
-
} = opts;
|
|
907
|
-
|
|
908
|
-
// Определение разделителя
|
|
909
|
-
let finalDelimiter = delimiter;
|
|
910
|
-
if (!finalDelimiter && autoDetect) {
|
|
911
|
-
finalDelimiter = autoDetectDelimiter(csv, candidates);
|
|
519
|
+
* Асинхронная версия jsonToCsvSafe
|
|
520
|
+
*/
|
|
521
|
+
async function jsonToCsvSafeAsync(data, options = {}) {
|
|
522
|
+
try {
|
|
523
|
+
return await jsonToCsvAsync$1(data, options);
|
|
912
524
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (csv.trim() === '') {
|
|
917
|
-
return [];
|
|
918
|
-
}
|
|
919
|
-
if (isSimpleCsv(csv)) {
|
|
920
|
-
return parseSimpleCsv(csv, finalDelimiter, {
|
|
921
|
-
hasHeaders,
|
|
922
|
-
renameMap,
|
|
923
|
-
trim,
|
|
924
|
-
parseNumbers,
|
|
925
|
-
parseBooleans,
|
|
926
|
-
maxRows
|
|
927
|
-
});
|
|
525
|
+
catch (error) {
|
|
526
|
+
console.error('JSON to CSV conversion error:', error);
|
|
527
|
+
return null;
|
|
928
528
|
}
|
|
529
|
+
}
|
|
530
|
+
// Экспорт для Node.js совместимости
|
|
531
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
532
|
+
module.exports = {
|
|
533
|
+
jsonToCsv: jsonToCsv$1,
|
|
534
|
+
jsonToCsvAsync: jsonToCsvAsync$1,
|
|
535
|
+
jsonToCsvIterator,
|
|
536
|
+
jsonToCsvIteratorAsync,
|
|
537
|
+
jsonToCsvSafe,
|
|
538
|
+
jsonToCsvSafeAsync
|
|
539
|
+
};
|
|
540
|
+
}
|
|
929
541
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
currentLine += '"';
|
|
940
|
-
i++; // Пропустить следующую кавычку
|
|
941
|
-
} else {
|
|
942
|
-
// Переключение режима кавычек
|
|
943
|
-
insideQuotes = !insideQuotes;
|
|
944
|
-
}
|
|
945
|
-
currentLine += char;
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
if (char === '\n' && !insideQuotes) {
|
|
949
|
-
// Конец строки (вне кавычек)
|
|
950
|
-
lines.push(currentLine);
|
|
951
|
-
currentLine = '';
|
|
952
|
-
continue;
|
|
953
|
-
}
|
|
954
|
-
if (char === '\r') {
|
|
955
|
-
// Игнорировать carriage return
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
currentLine += char;
|
|
959
|
-
}
|
|
542
|
+
var jsonToCsvBrowser = /*#__PURE__*/Object.freeze({
|
|
543
|
+
__proto__: null,
|
|
544
|
+
jsonToCsv: jsonToCsv$1,
|
|
545
|
+
jsonToCsvAsync: jsonToCsvAsync$1,
|
|
546
|
+
jsonToCsvIterator: jsonToCsvIterator,
|
|
547
|
+
jsonToCsvIteratorAsync: jsonToCsvIteratorAsync,
|
|
548
|
+
jsonToCsvSafe: jsonToCsvSafe,
|
|
549
|
+
jsonToCsvSafeAsync: jsonToCsvSafeAsync
|
|
550
|
+
});
|
|
960
551
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
552
|
+
// Браузерная версия CSV to JSON конвертера
|
|
553
|
+
// Адаптирована для работы в браузере без Node.js API
|
|
554
|
+
/**
|
|
555
|
+
* Валидация опций парсинга
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
function validateCsvOptions(options) {
|
|
559
|
+
// Validate options
|
|
560
|
+
if (options && typeof options !== 'object') {
|
|
561
|
+
throw new ConfigurationError('Options must be an object');
|
|
964
562
|
}
|
|
965
|
-
|
|
966
|
-
|
|
563
|
+
// Validate delimiter
|
|
564
|
+
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
565
|
+
throw new ConfigurationError('Delimiter must be a string');
|
|
967
566
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (lines.length > 1000000 && !maxRows && process.env.NODE_ENV !== 'production') {
|
|
971
|
-
console.warn('⚠️ Warning: Processing >1M records in memory may be slow.\n' + '💡 Consider using Web Workers for better performance with large files.\n' + '📊 Current size: ' + lines.length.toLocaleString() + ' rows');
|
|
567
|
+
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
568
|
+
throw new ConfigurationError('Delimiter must be a single character');
|
|
972
569
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
throw new LimitError(`CSV size exceeds maximum limit of ${maxRows} rows`, maxRows, lines.length);
|
|
570
|
+
// Validate autoDetect
|
|
571
|
+
if (options?.autoDetect !== undefined && typeof options.autoDetect !== 'boolean') {
|
|
572
|
+
throw new ConfigurationError('autoDetect must be a boolean');
|
|
977
573
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
// Парсинг заголовков если есть
|
|
982
|
-
if (hasHeaders && lines.length > 0) {
|
|
983
|
-
try {
|
|
984
|
-
headers = parseCsvLine(lines[0], 1, finalDelimiter).map(header => {
|
|
985
|
-
const trimmed = trim ? header.trim() : header;
|
|
986
|
-
return renameMap[trimmed] || trimmed;
|
|
987
|
-
});
|
|
988
|
-
startIndex = 1;
|
|
989
|
-
} catch (error) {
|
|
990
|
-
if (error instanceof ParsingError) {
|
|
991
|
-
throw new ParsingError(`Failed to parse headers: ${error.message}`, 1);
|
|
992
|
-
}
|
|
993
|
-
throw error;
|
|
994
|
-
}
|
|
995
|
-
} else {
|
|
996
|
-
// Генерация числовых заголовков из первой строки
|
|
997
|
-
try {
|
|
998
|
-
const firstLineFields = parseCsvLine(lines[0], 1, finalDelimiter);
|
|
999
|
-
headers = firstLineFields.map((_, index) => `column${index + 1}`);
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
if (error instanceof ParsingError) {
|
|
1002
|
-
throw new ParsingError(`Failed to parse first line: ${error.message}`, 1);
|
|
1003
|
-
}
|
|
1004
|
-
throw error;
|
|
1005
|
-
}
|
|
574
|
+
// Validate candidates
|
|
575
|
+
if (options?.candidates && !Array.isArray(options.candidates)) {
|
|
576
|
+
throw new ConfigurationError('candidates must be an array');
|
|
1006
577
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (
|
|
1039
|
-
|
|
578
|
+
// Validate maxRows
|
|
579
|
+
if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
|
|
580
|
+
throw new ConfigurationError('maxRows must be a positive number');
|
|
581
|
+
}
|
|
582
|
+
if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
|
|
583
|
+
throw new ConfigurationError('warnExtraFields must be a boolean');
|
|
584
|
+
}
|
|
585
|
+
if (options?.repairRowShifts !== undefined && typeof options.repairRowShifts !== 'boolean') {
|
|
586
|
+
throw new ConfigurationError('repairRowShifts must be a boolean');
|
|
587
|
+
}
|
|
588
|
+
if (options?.normalizeQuotes !== undefined && typeof options.normalizeQuotes !== 'boolean') {
|
|
589
|
+
throw new ConfigurationError('normalizeQuotes must be a boolean');
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Автоматическое определение разделителя
|
|
595
|
+
* @private
|
|
596
|
+
*/
|
|
597
|
+
function autoDetectDelimiter$1(text, candidates = [',', ';', '\t', '|']) {
|
|
598
|
+
if (!text || typeof text !== 'string') {
|
|
599
|
+
return ',';
|
|
600
|
+
}
|
|
601
|
+
const firstLine = text.split('\n')[0];
|
|
602
|
+
if (!firstLine) {
|
|
603
|
+
return ',';
|
|
604
|
+
}
|
|
605
|
+
let bestCandidate = ',';
|
|
606
|
+
let bestCount = 0;
|
|
607
|
+
for (const candidate of candidates) {
|
|
608
|
+
const count = (firstLine.match(new RegExp(candidate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
|
609
|
+
if (count > bestCount) {
|
|
610
|
+
bestCount = count;
|
|
611
|
+
bestCandidate = candidate;
|
|
1040
612
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
return result;
|
|
1045
|
-
}, 'PARSE_FAILED', {
|
|
1046
|
-
function: 'csvToJson'
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
async function* csvToJsonIterator(input, options = {}) {
|
|
1050
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
1051
|
-
validateCsvOptions(opts);
|
|
1052
|
-
if (typeof input === 'string') {
|
|
1053
|
-
const rows = csvToJson(input, options);
|
|
1054
|
-
for (const row of rows) {
|
|
1055
|
-
yield row;
|
|
1056
|
-
}
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
const {
|
|
1060
|
-
delimiter,
|
|
1061
|
-
autoDetect = true,
|
|
1062
|
-
candidates = [';', ',', '\t', '|'],
|
|
1063
|
-
hasHeaders = true,
|
|
1064
|
-
renameMap = {},
|
|
1065
|
-
trim = true,
|
|
1066
|
-
parseNumbers = false,
|
|
1067
|
-
parseBooleans = false,
|
|
1068
|
-
maxRows
|
|
1069
|
-
} = opts;
|
|
1070
|
-
const stream = input instanceof Blob && input.stream ? input.stream() : input;
|
|
1071
|
-
if (!stream || typeof stream.getReader !== 'function') {
|
|
1072
|
-
throw new ValidationError('Input must be a CSV string, Blob/File, or ReadableStream');
|
|
1073
|
-
}
|
|
1074
|
-
const reader = stream.getReader();
|
|
1075
|
-
const decoder = new TextDecoder('utf-8');
|
|
1076
|
-
let buffer = '';
|
|
1077
|
-
let insideQuotes = false;
|
|
1078
|
-
let headers = null;
|
|
1079
|
-
let rowCount = 0;
|
|
1080
|
-
let lineNumber = 0;
|
|
1081
|
-
let finalDelimiter = delimiter;
|
|
1082
|
-
let delimiterResolved = Boolean(finalDelimiter);
|
|
1083
|
-
const processFields = fields => {
|
|
1084
|
-
if (fields.length === 1 && fields[0].trim() === '') {
|
|
1085
|
-
return null;
|
|
1086
|
-
}
|
|
1087
|
-
rowCount++;
|
|
1088
|
-
if (maxRows && rowCount > maxRows) {
|
|
1089
|
-
throw new LimitError(`CSV size exceeds maximum limit of ${maxRows} rows`, maxRows, rowCount);
|
|
1090
|
-
}
|
|
1091
|
-
const row = {};
|
|
1092
|
-
const fieldCount = Math.min(fields.length, headers.length);
|
|
1093
|
-
for (let j = 0; j < fieldCount; j++) {
|
|
1094
|
-
row[headers[j]] = parseCsvValue(fields[j], {
|
|
1095
|
-
trim,
|
|
1096
|
-
parseNumbers,
|
|
1097
|
-
parseBooleans
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
return row;
|
|
1101
|
-
};
|
|
1102
|
-
const processLine = line => {
|
|
1103
|
-
lineNumber++;
|
|
1104
|
-
let cleanLine = line;
|
|
1105
|
-
if (cleanLine.endsWith('\r')) {
|
|
1106
|
-
cleanLine = cleanLine.slice(0, -1);
|
|
1107
|
-
}
|
|
1108
|
-
if (!delimiterResolved) {
|
|
1109
|
-
if (!finalDelimiter && autoDetect) {
|
|
1110
|
-
finalDelimiter = autoDetectDelimiter(cleanLine, candidates);
|
|
1111
|
-
}
|
|
1112
|
-
finalDelimiter = finalDelimiter || ';';
|
|
1113
|
-
delimiterResolved = true;
|
|
1114
|
-
}
|
|
1115
|
-
if (cleanLine.trim() === '') {
|
|
1116
|
-
return null;
|
|
1117
|
-
}
|
|
1118
|
-
if (!headers) {
|
|
1119
|
-
if (hasHeaders) {
|
|
1120
|
-
headers = parseCsvLine(cleanLine, lineNumber, finalDelimiter).map(header => {
|
|
1121
|
-
const trimmed = trim ? header.trim() : header;
|
|
1122
|
-
return renameMap[trimmed] || trimmed;
|
|
1123
|
-
});
|
|
1124
|
-
return null;
|
|
1125
|
-
}
|
|
1126
|
-
const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
|
|
1127
|
-
headers = fields.map((_, index) => `column${index + 1}`);
|
|
1128
|
-
return processFields(fields);
|
|
1129
|
-
}
|
|
1130
|
-
const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
|
|
1131
|
-
return processFields(fields);
|
|
1132
|
-
};
|
|
1133
|
-
while (true) {
|
|
1134
|
-
const {
|
|
1135
|
-
value,
|
|
1136
|
-
done
|
|
1137
|
-
} = await reader.read();
|
|
1138
|
-
if (done) {
|
|
1139
|
-
break;
|
|
1140
|
-
}
|
|
1141
|
-
buffer += decoder.decode(value, {
|
|
1142
|
-
stream: true
|
|
1143
|
-
});
|
|
1144
|
-
let start = 0;
|
|
1145
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
1146
|
-
const char = buffer[i];
|
|
1147
|
-
if (char === '"') {
|
|
1148
|
-
if (insideQuotes && buffer[i + 1] === '"') {
|
|
1149
|
-
i++;
|
|
1150
|
-
continue;
|
|
1151
|
-
}
|
|
1152
|
-
insideQuotes = !insideQuotes;
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
if (char === '\n' && !insideQuotes) {
|
|
1156
|
-
const line = buffer.slice(start, i);
|
|
1157
|
-
start = i + 1;
|
|
1158
|
-
const row = processLine(line);
|
|
1159
|
-
if (row) {
|
|
1160
|
-
yield row;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
buffer = buffer.slice(start);
|
|
1165
|
-
}
|
|
1166
|
-
if (buffer.length > 0) {
|
|
1167
|
-
const row = processLine(buffer);
|
|
1168
|
-
if (row) {
|
|
1169
|
-
yield row;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
if (insideQuotes) {
|
|
1173
|
-
throw new ParsingError('Unclosed quotes in CSV', lineNumber);
|
|
1174
|
-
}
|
|
613
|
+
}
|
|
614
|
+
return bestCandidate;
|
|
1175
615
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
1179
|
-
module.exports = {
|
|
1180
|
-
csvToJson,
|
|
1181
|
-
autoDetectDelimiter,
|
|
1182
|
-
csvToJsonIterator
|
|
1183
|
-
};
|
|
616
|
+
function isEmptyValue(value) {
|
|
617
|
+
return value === undefined || value === null || value === '';
|
|
1184
618
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
619
|
+
function hasOddQuotes(value) {
|
|
620
|
+
if (typeof value !== 'string') {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
let count = 0;
|
|
624
|
+
for (let i = 0; i < value.length; i++) {
|
|
625
|
+
if (value[i] === '"') {
|
|
626
|
+
count++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return count % 2 === 1;
|
|
1189
630
|
}
|
|
1190
|
-
function
|
|
1191
|
-
|
|
631
|
+
function hasAnyQuotes(value) {
|
|
632
|
+
return typeof value === 'string' && value.includes('"');
|
|
1192
633
|
}
|
|
1193
|
-
function
|
|
1194
|
-
|
|
634
|
+
function normalizeQuotesInField$1(value) {
|
|
635
|
+
if (typeof value !== 'string') {
|
|
636
|
+
return value;
|
|
637
|
+
}
|
|
638
|
+
// Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
|
|
639
|
+
// Проверяем, выглядит ли значение как JSON (объект или массив)
|
|
640
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
641
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
642
|
+
return value; // Возвращаем как есть для JSON
|
|
643
|
+
}
|
|
644
|
+
let normalized = value.replace(/"{2,}/g, '"');
|
|
645
|
+
// Убираем правило, которое ломает JSON: не заменяем "," на ","
|
|
646
|
+
// normalized = normalized.replace(/"\s*,\s*"/g, ',');
|
|
647
|
+
normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
|
|
648
|
+
if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
649
|
+
normalized = normalized.slice(1, -1);
|
|
650
|
+
}
|
|
651
|
+
return normalized;
|
|
1195
652
|
}
|
|
1196
|
-
function
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
controller.close();
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
controller.enqueue(value);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
controller.error(error);
|
|
1211
|
-
}
|
|
1212
|
-
},
|
|
1213
|
-
cancel() {
|
|
1214
|
-
if (iterator.return) {
|
|
1215
|
-
iterator.return();
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
653
|
+
function normalizePhoneValue$1(value) {
|
|
654
|
+
if (typeof value !== 'string') {
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
const trimmed = value.trim();
|
|
658
|
+
if (trimmed === '') {
|
|
659
|
+
return trimmed;
|
|
660
|
+
}
|
|
661
|
+
return trimmed.replace(/["'\\]/g, '');
|
|
1219
662
|
}
|
|
1220
|
-
function
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
}
|
|
1232
|
-
return
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
663
|
+
function normalizeRowQuotes(row, headers) {
|
|
664
|
+
const normalized = {};
|
|
665
|
+
const phoneKeys = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
|
|
666
|
+
for (const header of headers) {
|
|
667
|
+
const baseValue = normalizeQuotesInField$1(row[header]);
|
|
668
|
+
if (phoneKeys.has(String(header).toLowerCase())) {
|
|
669
|
+
normalized[header] = normalizePhoneValue$1(baseValue);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
normalized[header] = baseValue;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return normalized;
|
|
676
|
+
}
|
|
677
|
+
function looksLikeUserAgent(value) {
|
|
678
|
+
if (typeof value !== 'string') {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
return /Mozilla\/|Opera\/|MSIE|AppleWebKit|Gecko|Safari|Chrome\//.test(value);
|
|
682
|
+
}
|
|
683
|
+
function isHexColor(value) {
|
|
684
|
+
return typeof value === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
|
685
|
+
}
|
|
686
|
+
function repairShiftedRows(rows, headers, options = {}) {
|
|
687
|
+
if (!Array.isArray(rows) || rows.length === 0 || headers.length === 0) {
|
|
688
|
+
return rows;
|
|
689
|
+
}
|
|
690
|
+
const headerCount = headers.length;
|
|
691
|
+
const merged = [];
|
|
692
|
+
let index = 0;
|
|
693
|
+
while (index < rows.length) {
|
|
694
|
+
const row = rows[index];
|
|
695
|
+
if (!row || typeof row !== 'object') {
|
|
696
|
+
merged.push(row);
|
|
697
|
+
index++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const values = headers.map((header) => row[header]);
|
|
701
|
+
let lastNonEmpty = -1;
|
|
702
|
+
for (let i = headerCount - 1; i >= 0; i--) {
|
|
703
|
+
if (!isEmptyValue(values[i])) {
|
|
704
|
+
lastNonEmpty = i;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const missingCount = headerCount - 1 - lastNonEmpty;
|
|
709
|
+
if (lastNonEmpty >= 0 && missingCount > 0 && index + 1 < rows.length) {
|
|
710
|
+
const nextRow = rows[index + 1];
|
|
711
|
+
if (nextRow && typeof nextRow === 'object') {
|
|
712
|
+
const nextValues = headers.map((header) => nextRow[header]);
|
|
713
|
+
const nextTrailingEmpty = nextValues
|
|
714
|
+
.slice(headerCount - missingCount)
|
|
715
|
+
.every((value) => isEmptyValue(value));
|
|
716
|
+
const leadValues = nextValues
|
|
717
|
+
.slice(0, missingCount)
|
|
718
|
+
.filter((value) => !isEmptyValue(value));
|
|
719
|
+
const shouldMerge = nextTrailingEmpty
|
|
720
|
+
&& leadValues.length > 0
|
|
721
|
+
&& (hasOddQuotes(values[lastNonEmpty]) || hasAnyQuotes(values[lastNonEmpty]));
|
|
722
|
+
if (shouldMerge) {
|
|
723
|
+
const toAppend = leadValues.map((value) => String(value));
|
|
724
|
+
if (toAppend.length > 0) {
|
|
725
|
+
const base = isEmptyValue(values[lastNonEmpty]) ? '' : String(values[lastNonEmpty]);
|
|
726
|
+
values[lastNonEmpty] = base ? `${base}\n${toAppend.join('\n')}` : toAppend.join('\n');
|
|
727
|
+
}
|
|
728
|
+
for (let i = 0; i < missingCount; i++) {
|
|
729
|
+
values[lastNonEmpty + 1 + i] = nextValues[missingCount + i];
|
|
730
|
+
}
|
|
731
|
+
const mergedRow = {};
|
|
732
|
+
for (let i = 0; i < headerCount; i++) {
|
|
733
|
+
mergedRow[headers[i]] = values[i];
|
|
734
|
+
}
|
|
735
|
+
merged.push(mergedRow);
|
|
736
|
+
index += 2;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (index + 1 < rows.length && headerCount >= 6) {
|
|
742
|
+
const nextRow = rows[index + 1];
|
|
743
|
+
if (nextRow && typeof nextRow === 'object') {
|
|
744
|
+
const nextHex = nextRow[headers[4]];
|
|
745
|
+
const nextUserAgentHead = nextRow[headers[2]];
|
|
746
|
+
const nextUserAgentTail = nextRow[headers[3]];
|
|
747
|
+
const shouldMergeUserAgent = isEmptyValue(values[4])
|
|
748
|
+
&& isEmptyValue(values[5])
|
|
749
|
+
&& isHexColor(nextHex)
|
|
750
|
+
&& (looksLikeUserAgent(nextUserAgentHead) || looksLikeUserAgent(nextUserAgentTail));
|
|
751
|
+
if (shouldMergeUserAgent) {
|
|
752
|
+
const addressParts = [values[3], nextRow[headers[0]], nextRow[headers[1]]]
|
|
753
|
+
.filter((value) => !isEmptyValue(value))
|
|
754
|
+
.map((value) => String(value));
|
|
755
|
+
values[3] = addressParts.join('\n');
|
|
756
|
+
const uaHead = isEmptyValue(nextUserAgentHead) ? '' : String(nextUserAgentHead);
|
|
757
|
+
const uaTail = isEmptyValue(nextUserAgentTail) ? '' : String(nextUserAgentTail);
|
|
758
|
+
const joiner = uaHead && uaTail ? (uaTail.startsWith(' ') ? '' : ',') : '';
|
|
759
|
+
values[4] = uaHead + joiner + uaTail;
|
|
760
|
+
values[5] = String(nextHex);
|
|
761
|
+
const mergedRow = {};
|
|
762
|
+
for (let i = 0; i < headerCount; i++) {
|
|
763
|
+
mergedRow[headers[i]] = values[i];
|
|
764
|
+
}
|
|
765
|
+
merged.push(mergedRow);
|
|
766
|
+
index += 2;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
merged.push(row);
|
|
772
|
+
index++;
|
|
773
|
+
}
|
|
774
|
+
if (options.normalizeQuotes) {
|
|
775
|
+
return merged.map((row) => normalizeRowQuotes(row, headers));
|
|
776
|
+
}
|
|
777
|
+
return merged;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Парсинг CSV строки в массив объектов
|
|
781
|
+
*
|
|
782
|
+
* @param csvText - CSV текст для парсинга
|
|
783
|
+
* @param options - Опции парсинга
|
|
784
|
+
* @returns Массив объектов
|
|
785
|
+
*/
|
|
786
|
+
function csvToJson$1(csvText, options = {}) {
|
|
787
|
+
return safeExecute(() => {
|
|
788
|
+
validateCsvOptions(options);
|
|
789
|
+
if (typeof csvText !== 'string') {
|
|
790
|
+
throw new ValidationError('CSV text must be a string');
|
|
791
|
+
}
|
|
792
|
+
if (csvText.trim() === '') {
|
|
793
|
+
return [];
|
|
794
|
+
}
|
|
795
|
+
// Определение разделителя
|
|
796
|
+
const delimiter = options.delimiter ||
|
|
797
|
+
(options.autoDetect !== false ? autoDetectDelimiter$1(csvText, options.candidates) : ',');
|
|
798
|
+
// Разделение на строки
|
|
799
|
+
const lines = csvText.split('\n').filter(line => line.trim() !== '');
|
|
800
|
+
if (lines.length === 0) {
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
// Парсинг заголовков
|
|
804
|
+
const headers = lines[0].split(delimiter).map(h => h.trim());
|
|
805
|
+
const { repairRowShifts = true, normalizeQuotes = true } = options || {};
|
|
806
|
+
// Ограничение количества строк
|
|
807
|
+
const maxRows = options.maxRows || Infinity;
|
|
808
|
+
const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
|
|
809
|
+
// Парсинг данных
|
|
810
|
+
const result = [];
|
|
811
|
+
for (let i = 0; i < dataRows.length; i++) {
|
|
812
|
+
const line = dataRows[i];
|
|
813
|
+
const values = line.split(delimiter);
|
|
814
|
+
const row = {};
|
|
815
|
+
for (let j = 0; j < headers.length; j++) {
|
|
816
|
+
const header = headers[j];
|
|
817
|
+
const value = j < values.length ? values[j].trim() : '';
|
|
818
|
+
// Попытка парсинга чисел
|
|
819
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
820
|
+
row[header] = parseFloat(value);
|
|
821
|
+
}
|
|
822
|
+
else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
|
823
|
+
row[header] = value.toLowerCase() === 'true';
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
row[header] = value;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
result.push(row);
|
|
830
|
+
}
|
|
831
|
+
if (repairRowShifts) {
|
|
832
|
+
return repairShiftedRows(result, headers, { normalizeQuotes });
|
|
833
|
+
}
|
|
834
|
+
if (normalizeQuotes) {
|
|
835
|
+
return result.map((row) => normalizeRowQuotes(row, headers));
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Асинхронная версия csvToJson
|
|
842
|
+
*/
|
|
843
|
+
async function csvToJsonAsync$1(csvText, options = {}) {
|
|
844
|
+
return csvToJson$1(csvText, options);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Создает итератор для потокового парсинга CSV
|
|
848
|
+
*
|
|
849
|
+
* @param input - CSV текст, File или Blob
|
|
850
|
+
* @param options - Опции парсинга
|
|
851
|
+
* @returns AsyncGenerator
|
|
852
|
+
*/
|
|
853
|
+
async function* csvToJsonIterator$1(input, options = {}) {
|
|
854
|
+
validateCsvOptions(options);
|
|
855
|
+
let csvText;
|
|
856
|
+
if (typeof input === 'string') {
|
|
857
|
+
csvText = input;
|
|
858
|
+
}
|
|
859
|
+
else if (input instanceof File || input instanceof Blob) {
|
|
860
|
+
csvText = await input.text();
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
throw new ValidationError('Input must be string, File or Blob');
|
|
864
|
+
}
|
|
865
|
+
if (csvText.trim() === '') {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Определение разделителя
|
|
869
|
+
const delimiter = options.delimiter ||
|
|
870
|
+
(options.autoDetect !== false ? autoDetectDelimiter$1(csvText, options.candidates) : ',');
|
|
871
|
+
// Разделение на строки
|
|
872
|
+
const lines = csvText.split('\n').filter(line => line.trim() !== '');
|
|
873
|
+
if (lines.length === 0) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
// Парсинг заголовков
|
|
877
|
+
const headers = lines[0].split(delimiter).map(h => h.trim());
|
|
878
|
+
const { repairRowShifts = true, normalizeQuotes = true } = options || {};
|
|
879
|
+
// Ограничение количества строк
|
|
880
|
+
const maxRows = options.maxRows || Infinity;
|
|
881
|
+
const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
|
|
882
|
+
// Возврат данных по одной строке
|
|
883
|
+
const parsedRows = [];
|
|
884
|
+
for (let i = 0; i < dataRows.length; i++) {
|
|
885
|
+
const line = dataRows[i];
|
|
886
|
+
const values = line.split(delimiter);
|
|
887
|
+
const row = {};
|
|
888
|
+
for (let j = 0; j < headers.length; j++) {
|
|
889
|
+
const header = headers[j];
|
|
890
|
+
const value = j < values.length ? values[j].trim() : '';
|
|
891
|
+
// Try parsing numbers
|
|
892
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
893
|
+
row[header] = parseFloat(value);
|
|
894
|
+
}
|
|
895
|
+
else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
|
896
|
+
row[header] = value.toLowerCase() === 'true';
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
row[header] = value;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
parsedRows.push(row);
|
|
903
|
+
}
|
|
904
|
+
const finalRows = repairRowShifts
|
|
905
|
+
? repairShiftedRows(parsedRows, headers, { normalizeQuotes })
|
|
906
|
+
: (normalizeQuotes
|
|
907
|
+
? parsedRows.map((row) => normalizeRowQuotes(row, headers))
|
|
908
|
+
: parsedRows);
|
|
909
|
+
for (const row of finalRows) {
|
|
910
|
+
yield row;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Асинхронная версия csvToJsonIterator (псевдоним)
|
|
915
|
+
*/
|
|
916
|
+
const csvToJsonIteratorAsync = csvToJsonIterator$1;
|
|
917
|
+
/**
|
|
918
|
+
* Парсинг CSV с обработкой ошибок
|
|
919
|
+
*
|
|
920
|
+
* @param csvText - CSV текст
|
|
921
|
+
* @param options - Опции парсинга
|
|
922
|
+
* @returns Результат парсинга или null при ошибке
|
|
923
|
+
*/
|
|
924
|
+
function parseCsvSafe(csvText, options = {}) {
|
|
925
|
+
try {
|
|
926
|
+
return csvToJson$1(csvText, options);
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
console.error('CSV parsing error:', error);
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Асинхронная версия parseCsvSafe
|
|
935
|
+
*/
|
|
936
|
+
async function parseCsvSafeAsync(csvText, options = {}) {
|
|
937
|
+
try {
|
|
938
|
+
return await csvToJsonAsync$1(csvText, options);
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
console.error('CSV parsing error:', error);
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Экспорт для Node.js совместимости
|
|
946
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
947
|
+
module.exports = {
|
|
948
|
+
csvToJson: csvToJson$1,
|
|
949
|
+
csvToJsonAsync: csvToJsonAsync$1,
|
|
950
|
+
csvToJsonIterator: csvToJsonIterator$1,
|
|
951
|
+
csvToJsonIteratorAsync,
|
|
952
|
+
parseCsvSafe,
|
|
953
|
+
parseCsvSafeAsync,
|
|
954
|
+
autoDetectDelimiter: autoDetectDelimiter$1
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
var csvToJsonBrowser = /*#__PURE__*/Object.freeze({
|
|
959
|
+
__proto__: null,
|
|
960
|
+
csvToJson: csvToJson$1,
|
|
961
|
+
csvToJsonAsync: csvToJsonAsync$1,
|
|
962
|
+
csvToJsonIterator: csvToJsonIterator$1,
|
|
963
|
+
csvToJsonIteratorAsync: csvToJsonIteratorAsync,
|
|
964
|
+
parseCsvSafe: parseCsvSafe,
|
|
965
|
+
parseCsvSafeAsync: parseCsvSafeAsync
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
const PHONE_KEYS = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
|
|
969
|
+
function isReadableStream(value) {
|
|
970
|
+
return value && typeof value.getReader === 'function';
|
|
971
|
+
}
|
|
972
|
+
function isAsyncIterable(value) {
|
|
973
|
+
return value && typeof value[Symbol.asyncIterator] === 'function';
|
|
974
|
+
}
|
|
975
|
+
function isIterable(value) {
|
|
976
|
+
return value && typeof value[Symbol.iterator] === 'function';
|
|
977
|
+
}
|
|
978
|
+
function createReadableStreamFromIterator(iterator) {
|
|
979
|
+
return new ReadableStream({
|
|
980
|
+
async pull(controller) {
|
|
981
|
+
try {
|
|
982
|
+
const { value, done } = await iterator.next();
|
|
983
|
+
if (done) {
|
|
984
|
+
controller.close();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
controller.enqueue(value);
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
controller.error(error);
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
cancel() {
|
|
994
|
+
if (iterator.return) {
|
|
995
|
+
iterator.return();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
1263
998
|
});
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
throw new ValidationError('Input must be an array, iterable, string, Blob, or ReadableStream');
|
|
1337
|
-
}
|
|
1338
|
-
function validateStreamOptions(options) {
|
|
1339
|
-
if (options && typeof options !== 'object') {
|
|
1340
|
-
throw new ConfigurationError('Options must be an object');
|
|
1341
|
-
}
|
|
1342
|
-
if (options?.delimiter && typeof options.delimiter !== 'string') {
|
|
1343
|
-
throw new ConfigurationError('Delimiter must be a string');
|
|
1344
|
-
}
|
|
1345
|
-
if (options?.delimiter && options.delimiter.length !== 1) {
|
|
1346
|
-
throw new ConfigurationError('Delimiter must be a single character');
|
|
1347
|
-
}
|
|
1348
|
-
if (options?.renameMap && typeof options.renameMap !== 'object') {
|
|
1349
|
-
throw new ConfigurationError('renameMap must be an object');
|
|
1350
|
-
}
|
|
1351
|
-
if (options?.maxRecords !== undefined) {
|
|
1352
|
-
if (typeof options.maxRecords !== 'number' || options.maxRecords <= 0) {
|
|
1353
|
-
throw new ConfigurationError('maxRecords must be a positive number');
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
function escapeCsvValue(value, options) {
|
|
1358
|
-
const {
|
|
1359
|
-
delimiter,
|
|
1360
|
-
preventCsvInjection = true,
|
|
1361
|
-
rfc4180Compliant = true
|
|
1362
|
-
} = options;
|
|
1363
|
-
if (value === null || value === undefined || value === '') {
|
|
1364
|
-
return '';
|
|
1365
|
-
}
|
|
1366
|
-
const stringValue = String(value);
|
|
1367
|
-
let escapedValue = stringValue;
|
|
1368
|
-
if (preventCsvInjection) {
|
|
1369
|
-
// Dangerous prefixes: =, +, -, @, tab (\t), carriage return (\r)
|
|
1370
|
-
if (/^[=+\-@\t\r]/.test(stringValue)) {
|
|
1371
|
-
escapedValue = "'" + stringValue;
|
|
1372
|
-
}
|
|
1373
|
-
// Unicode Bidi override characters
|
|
1374
|
-
const bidiChars = ['\u202A', '\u202B', '\u202C', '\u202D', '\u202E'];
|
|
1375
|
-
for (const bidi of bidiChars) {
|
|
1376
|
-
if (stringValue.includes(bidi)) {
|
|
1377
|
-
escapedValue = escapedValue.replace(new RegExp(bidi, 'g'), '');
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
const needsQuoting = rfc4180Compliant ? escapedValue.includes(delimiter) || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r') : escapedValue.includes(delimiter) || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r');
|
|
1382
|
-
if (needsQuoting) {
|
|
1383
|
-
return `"${escapedValue.replace(/"/g, '""')}"`;
|
|
1384
|
-
}
|
|
1385
|
-
return escapedValue;
|
|
1386
|
-
}
|
|
1387
|
-
function buildHeaderState(keys, options) {
|
|
1388
|
-
const renameMap = options.renameMap || {};
|
|
1389
|
-
const template = options.template || {};
|
|
1390
|
-
const originalKeys = Array.isArray(options.headers) ? options.headers : keys;
|
|
1391
|
-
const headers = originalKeys.map(key => renameMap[key] || key);
|
|
1392
|
-
const reverseRenameMap = {};
|
|
1393
|
-
originalKeys.forEach((key, index) => {
|
|
1394
|
-
reverseRenameMap[headers[index]] = key;
|
|
1395
|
-
});
|
|
1396
|
-
let finalHeaders = headers;
|
|
1397
|
-
if (Object.keys(template).length > 0) {
|
|
1398
|
-
const templateHeaders = Object.keys(template).map(key => renameMap[key] || key);
|
|
1399
|
-
const extraHeaders = headers.filter(h => !templateHeaders.includes(h));
|
|
1400
|
-
finalHeaders = [...templateHeaders, ...extraHeaders];
|
|
1401
|
-
}
|
|
1402
|
-
return {
|
|
1403
|
-
headers: finalHeaders,
|
|
1404
|
-
reverseRenameMap
|
|
1405
|
-
};
|
|
999
|
+
}
|
|
1000
|
+
function detectInputFormat(input, options) {
|
|
1001
|
+
if (options && options.inputFormat) {
|
|
1002
|
+
return options.inputFormat;
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof input === 'string') {
|
|
1005
|
+
const trimmed = input.trim();
|
|
1006
|
+
if (trimmed === '') {
|
|
1007
|
+
return 'unknown';
|
|
1008
|
+
}
|
|
1009
|
+
// Проверка на NDJSON (каждая строка - валидный JSON)
|
|
1010
|
+
if (trimmed.includes('\n')) {
|
|
1011
|
+
const lines = trimmed.split('\n').filter(line => line.trim() !== '');
|
|
1012
|
+
if (lines.length > 0) {
|
|
1013
|
+
try {
|
|
1014
|
+
JSON.parse(lines[0]);
|
|
1015
|
+
return 'ndjson';
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
// Не NDJSON
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// Проверка на JSON
|
|
1023
|
+
try {
|
|
1024
|
+
const parsed = JSON.parse(trimmed);
|
|
1025
|
+
if (Array.isArray(parsed) || (parsed && typeof parsed === 'object')) {
|
|
1026
|
+
return 'json';
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// Не JSON
|
|
1031
|
+
}
|
|
1032
|
+
// Проверка на CSV
|
|
1033
|
+
if (trimmed.includes(',') || trimmed.includes(';') || trimmed.includes('\t')) {
|
|
1034
|
+
return 'csv';
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return 'unknown';
|
|
1038
|
+
}
|
|
1039
|
+
function normalizeQuotesInField(value) {
|
|
1040
|
+
// Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
|
|
1041
|
+
// Проверяем, выглядит ли значение как JSON (объект или массив)
|
|
1042
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
1043
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
1044
|
+
return value; // Возвращаем как есть для JSON
|
|
1045
|
+
}
|
|
1046
|
+
let normalized = value.replace(/"{2,}/g, '"');
|
|
1047
|
+
// Убираем правило, которое ломает JSON: не заменяем "," на ","
|
|
1048
|
+
// normalized = normalized.replace(/"\s*,\s*"/g, ',');
|
|
1049
|
+
normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
|
|
1050
|
+
if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
1051
|
+
normalized = normalized.slice(1, -1);
|
|
1052
|
+
}
|
|
1053
|
+
return normalized;
|
|
1054
|
+
}
|
|
1055
|
+
function normalizePhoneValue(value) {
|
|
1056
|
+
const trimmed = value.trim();
|
|
1057
|
+
if (trimmed === '') {
|
|
1058
|
+
return trimmed;
|
|
1059
|
+
}
|
|
1060
|
+
return trimmed.replace(/["'\\]/g, '');
|
|
1061
|
+
}
|
|
1062
|
+
function normalizeValueForCsv(value, key, normalizeQuotes) {
|
|
1063
|
+
if (!normalizeQuotes || typeof value !== 'string') {
|
|
1064
|
+
return value;
|
|
1065
|
+
}
|
|
1066
|
+
const base = normalizeQuotesInField(value);
|
|
1067
|
+
if (key && PHONE_KEYS.has(String(key).toLowerCase())) {
|
|
1068
|
+
return normalizePhoneValue(base);
|
|
1069
|
+
}
|
|
1070
|
+
return base;
|
|
1406
1071
|
}
|
|
1407
1072
|
async function* jsonToCsvChunkIterator(input, options = {}) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1073
|
+
const format = detectInputFormat(input, options);
|
|
1074
|
+
if (format === 'csv') {
|
|
1075
|
+
throw new ValidationError('Input appears to be CSV, not JSON');
|
|
1076
|
+
}
|
|
1077
|
+
// Вспомогательная функция для создания асинхронного итератора
|
|
1078
|
+
function toAsyncIterator(iterable) {
|
|
1079
|
+
if (isAsyncIterable(iterable)) {
|
|
1080
|
+
return iterable[Symbol.asyncIterator]();
|
|
1081
|
+
}
|
|
1082
|
+
if (isIterable(iterable)) {
|
|
1083
|
+
const syncIterator = iterable[Symbol.iterator]();
|
|
1084
|
+
return {
|
|
1085
|
+
next: () => Promise.resolve(syncIterator.next()),
|
|
1086
|
+
return: syncIterator.return ? () => Promise.resolve(syncIterator.return()) : undefined,
|
|
1087
|
+
throw: syncIterator.throw ? (error) => Promise.resolve(syncIterator.throw(error)) : undefined
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
throw new ValidationError('Input is not iterable');
|
|
1091
|
+
}
|
|
1092
|
+
let iterator;
|
|
1093
|
+
if (isAsyncIterable(input) || isIterable(input)) {
|
|
1094
|
+
iterator = toAsyncIterator(input);
|
|
1095
|
+
}
|
|
1096
|
+
else if (typeof input === 'string') {
|
|
1097
|
+
const parsed = JSON.parse(input);
|
|
1098
|
+
if (Array.isArray(parsed)) {
|
|
1099
|
+
iterator = toAsyncIterator(parsed);
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
iterator = toAsyncIterator([parsed]);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
else if (Array.isArray(input)) {
|
|
1106
|
+
iterator = toAsyncIterator(input);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
iterator = toAsyncIterator([input]);
|
|
1110
|
+
}
|
|
1111
|
+
const delimiter = options.delimiter || ';';
|
|
1112
|
+
const includeHeaders = options.includeHeaders !== false;
|
|
1113
|
+
const preventInjection = options.preventCsvInjection !== false;
|
|
1114
|
+
const normalizeQuotes = options.normalizeQuotes !== false;
|
|
1115
|
+
const isPotentialFormula = (input) => {
|
|
1116
|
+
let idx = 0;
|
|
1117
|
+
while (idx < input.length) {
|
|
1118
|
+
const code = input.charCodeAt(idx);
|
|
1119
|
+
if (code === 32 || code === 9 || code === 10 || code === 13 || code === 0xfeff) {
|
|
1120
|
+
idx++;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
if (idx < input.length && (input[idx] === '"' || input[idx] === "'")) {
|
|
1126
|
+
idx++;
|
|
1127
|
+
while (idx < input.length) {
|
|
1128
|
+
const code = input.charCodeAt(idx);
|
|
1129
|
+
if (code === 32 || code === 9) {
|
|
1130
|
+
idx++;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (idx >= input.length) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const char = input[idx];
|
|
1140
|
+
return char === '=' || char === '+' || char === '-' || char === '@';
|
|
1141
|
+
};
|
|
1142
|
+
let isFirstChunk = true;
|
|
1143
|
+
let headers = [];
|
|
1144
|
+
while (true) {
|
|
1145
|
+
const { value, done } = await iterator.next();
|
|
1146
|
+
if (done)
|
|
1147
|
+
break;
|
|
1148
|
+
const item = value;
|
|
1149
|
+
if (isFirstChunk) {
|
|
1150
|
+
// Извлечение заголовков из первого элемента
|
|
1151
|
+
headers = Object.keys(item);
|
|
1152
|
+
if (includeHeaders) {
|
|
1153
|
+
const headerLine = headers.map(header => {
|
|
1154
|
+
const escaped = header.includes('"') ? `"${header.replace(/"/g, '""')}"` : header;
|
|
1155
|
+
return preventInjection && isPotentialFormula(escaped) ? `'${escaped}` : escaped;
|
|
1156
|
+
}).join(delimiter);
|
|
1157
|
+
yield headerLine + '\n';
|
|
1158
|
+
}
|
|
1159
|
+
isFirstChunk = false;
|
|
1160
|
+
}
|
|
1161
|
+
const row = headers.map(header => {
|
|
1162
|
+
const value = item[header];
|
|
1163
|
+
const normalized = normalizeValueForCsv(value, header, normalizeQuotes);
|
|
1164
|
+
const strValue = normalized === null || normalized === undefined ? '' : String(normalized);
|
|
1165
|
+
if (strValue.includes('"') || strValue.includes('\n') || strValue.includes('\r') || strValue.includes(delimiter)) {
|
|
1166
|
+
return `"${strValue.replace(/"/g, '""')}"`;
|
|
1167
|
+
}
|
|
1168
|
+
if (preventInjection && isPotentialFormula(strValue)) {
|
|
1169
|
+
return `'${strValue}`;
|
|
1170
|
+
}
|
|
1171
|
+
return strValue;
|
|
1172
|
+
}).join(delimiter);
|
|
1173
|
+
yield row + '\n';
|
|
1174
|
+
}
|
|
1470
1175
|
}
|
|
1471
1176
|
async function* jsonToNdjsonChunkIterator(input, options = {}) {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1177
|
+
const format = detectInputFormat(input, options);
|
|
1178
|
+
// Вспомогательная функция для создания асинхронного итератора
|
|
1179
|
+
function toAsyncIterator(iterable) {
|
|
1180
|
+
if (isAsyncIterable(iterable)) {
|
|
1181
|
+
return iterable[Symbol.asyncIterator]();
|
|
1182
|
+
}
|
|
1183
|
+
if (isIterable(iterable)) {
|
|
1184
|
+
const syncIterator = iterable[Symbol.iterator]();
|
|
1185
|
+
return {
|
|
1186
|
+
next: () => Promise.resolve(syncIterator.next()),
|
|
1187
|
+
return: syncIterator.return ? () => Promise.resolve(syncIterator.return()) : undefined,
|
|
1188
|
+
throw: syncIterator.throw ? (error) => Promise.resolve(syncIterator.throw(error)) : undefined
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
throw new ValidationError('Input is not iterable');
|
|
1192
|
+
}
|
|
1193
|
+
let iterator;
|
|
1194
|
+
if (isAsyncIterable(input) || isIterable(input)) {
|
|
1195
|
+
iterator = toAsyncIterator(input);
|
|
1196
|
+
}
|
|
1197
|
+
else if (typeof input === 'string') {
|
|
1198
|
+
if (format === 'ndjson') {
|
|
1199
|
+
const lines = input.split('\n').filter(line => line.trim() !== '');
|
|
1200
|
+
iterator = toAsyncIterator(lines);
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
const parsed = JSON.parse(input);
|
|
1204
|
+
if (Array.isArray(parsed)) {
|
|
1205
|
+
iterator = toAsyncIterator(parsed);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
iterator = toAsyncIterator([parsed]);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
else if (Array.isArray(input)) {
|
|
1213
|
+
iterator = toAsyncIterator(input);
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
iterator = toAsyncIterator([input]);
|
|
1217
|
+
}
|
|
1218
|
+
while (true) {
|
|
1219
|
+
const { value, done } = await iterator.next();
|
|
1220
|
+
if (done)
|
|
1221
|
+
break;
|
|
1222
|
+
let jsonStr;
|
|
1223
|
+
if (typeof value === 'string') {
|
|
1224
|
+
try {
|
|
1225
|
+
// Проверяем, является ли строка валидным JSON
|
|
1226
|
+
JSON.parse(value);
|
|
1227
|
+
jsonStr = value;
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
// Если нет, сериализуем как JSON
|
|
1231
|
+
jsonStr = JSON.stringify(value);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
jsonStr = JSON.stringify(value);
|
|
1236
|
+
}
|
|
1237
|
+
yield jsonStr + '\n';
|
|
1476
1238
|
}
|
|
1477
|
-
yield JSON.stringify(item) + '\n';
|
|
1478
|
-
}
|
|
1479
1239
|
}
|
|
1480
1240
|
async function* csvToJsonChunkIterator(input, options = {}) {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1241
|
+
if (typeof input === 'string') {
|
|
1242
|
+
// Используем csvToJsonIterator из csv-to-json-browser
|
|
1243
|
+
yield* csvToJsonIterator$1(input, options);
|
|
1244
|
+
}
|
|
1245
|
+
else if (input instanceof File || input instanceof Blob) {
|
|
1246
|
+
const text = await input.text();
|
|
1247
|
+
yield* csvToJsonIterator$1(text, options);
|
|
1248
|
+
}
|
|
1249
|
+
else if (isReadableStream(input)) {
|
|
1250
|
+
const reader = input.getReader();
|
|
1251
|
+
const decoder = new TextDecoder();
|
|
1252
|
+
let buffer = '';
|
|
1253
|
+
try {
|
|
1254
|
+
while (true) {
|
|
1255
|
+
const { value, done } = await reader.read();
|
|
1256
|
+
if (done)
|
|
1257
|
+
break;
|
|
1258
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1259
|
+
// Обработка буфера по строкам
|
|
1260
|
+
const lines = buffer.split('\n');
|
|
1261
|
+
buffer = lines.pop() || '';
|
|
1262
|
+
// TODO: Реализовать парсинг CSV из чанков
|
|
1263
|
+
// Пока просто возвращаем сырые строки
|
|
1264
|
+
for (const line of lines) {
|
|
1265
|
+
if (line.trim()) {
|
|
1266
|
+
yield { raw: line };
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// Обработка остатка буфера
|
|
1271
|
+
if (buffer.trim()) {
|
|
1272
|
+
yield { raw: buffer };
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
finally {
|
|
1276
|
+
reader.releaseLock();
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
throw new ValidationError('Unsupported input type for CSV streaming');
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function jsonToCsvStream$1(input, options = {}) {
|
|
1284
|
+
const iterator = jsonToCsvChunkIterator(input, options);
|
|
1285
|
+
return createReadableStreamFromIterator(iterator);
|
|
1286
|
+
}
|
|
1287
|
+
function jsonToNdjsonStream$1(input, options = {}) {
|
|
1288
|
+
const iterator = jsonToNdjsonChunkIterator(input, options);
|
|
1289
|
+
return createReadableStreamFromIterator(iterator);
|
|
1511
1290
|
}
|
|
1291
|
+
function csvToJsonStream$1(input, options = {}) {
|
|
1292
|
+
const iterator = csvToJsonChunkIterator(input, options);
|
|
1293
|
+
return createReadableStreamFromIterator(iterator);
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Асинхронная версия jsonToCsvStream
|
|
1297
|
+
*/
|
|
1298
|
+
async function jsonToCsvStreamAsync(input, options = {}) {
|
|
1299
|
+
return jsonToCsvStream$1(input, options);
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Асинхронная версия jsonToNdjsonStream
|
|
1303
|
+
*/
|
|
1304
|
+
async function jsonToNdjsonStreamAsync(input, options = {}) {
|
|
1305
|
+
return jsonToNdjsonStream$1(input, options);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Асинхронная версия csvToJsonStream
|
|
1309
|
+
*/
|
|
1310
|
+
async function csvToJsonStreamAsync(input, options = {}) {
|
|
1311
|
+
return csvToJsonStream$1(input, options);
|
|
1312
|
+
}
|
|
1313
|
+
// Экспорт для Node.js совместимости
|
|
1512
1314
|
if (typeof module !== 'undefined' && module.exports) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1315
|
+
module.exports = {
|
|
1316
|
+
jsonToCsvStream: jsonToCsvStream$1,
|
|
1317
|
+
jsonToCsvStreamAsync,
|
|
1318
|
+
jsonToNdjsonStream: jsonToNdjsonStream$1,
|
|
1319
|
+
jsonToNdjsonStreamAsync,
|
|
1320
|
+
csvToJsonStream: csvToJsonStream$1,
|
|
1321
|
+
csvToJsonStreamAsync,
|
|
1322
|
+
createReadableStreamFromIterator
|
|
1323
|
+
};
|
|
1518
1324
|
}
|
|
1519
1325
|
|
|
1520
1326
|
// Браузерные специфичные функции для jtcsv
|
|
1521
1327
|
// Функции, которые работают только в браузере
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
1328
|
/**
|
|
1525
1329
|
* Скачивает JSON данные как CSV файл
|
|
1526
|
-
*
|
|
1527
|
-
* @param
|
|
1528
|
-
* @param
|
|
1529
|
-
* @param
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1330
|
+
*
|
|
1331
|
+
* @param data - Массив объектов для конвертации
|
|
1332
|
+
* @param filename - Имя файла для скачивания (по умолчанию 'data.csv')
|
|
1333
|
+
* @param options - Опции для jsonToCsv
|
|
1334
|
+
*
|
|
1532
1335
|
* @example
|
|
1533
1336
|
* const data = [
|
|
1534
1337
|
* { id: 1, name: 'John' },
|
|
@@ -1537,771 +1340,870 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
1537
1340
|
* downloadAsCsv(data, 'users.csv', { delimiter: ',' });
|
|
1538
1341
|
*/
|
|
1539
1342
|
function downloadAsCsv(data, filename = 'data.csv', options = {}) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
// Создание URL для Blob
|
|
1567
|
-
const url = URL.createObjectURL(blob);
|
|
1568
|
-
|
|
1569
|
-
// Настройка ссылки
|
|
1570
|
-
link.setAttribute('href', url);
|
|
1571
|
-
link.setAttribute('download', filename);
|
|
1572
|
-
link.style.visibility = 'hidden';
|
|
1573
|
-
|
|
1574
|
-
// Добавление в DOM и клик
|
|
1575
|
-
document.body.appendChild(link);
|
|
1576
|
-
link.click();
|
|
1577
|
-
|
|
1578
|
-
// Очистка
|
|
1579
|
-
setTimeout(() => {
|
|
1343
|
+
// Проверка что мы в браузере
|
|
1344
|
+
if (typeof window === 'undefined') {
|
|
1345
|
+
throw new ValidationError('downloadAsCsv() работает только в браузере. Используйте saveAsCsv() в Node.js');
|
|
1346
|
+
}
|
|
1347
|
+
// Валидация имени файла
|
|
1348
|
+
if (typeof filename !== 'string' || filename.trim() === '') {
|
|
1349
|
+
throw new ValidationError('Filename must be a non-empty string');
|
|
1350
|
+
}
|
|
1351
|
+
// Добавление расширения .csv если его нет
|
|
1352
|
+
if (!filename.toLowerCase().endsWith('.csv')) {
|
|
1353
|
+
filename += '.csv';
|
|
1354
|
+
}
|
|
1355
|
+
// Конвертация в CSV
|
|
1356
|
+
const csv = jsonToCsv$1(data, options);
|
|
1357
|
+
// Создание Blob
|
|
1358
|
+
const blob = new Blob([csv], {
|
|
1359
|
+
type: 'text/csv;charset=utf-8;'
|
|
1360
|
+
});
|
|
1361
|
+
// Создание ссылки для скачивания
|
|
1362
|
+
const link = document.createElement('a');
|
|
1363
|
+
const url = URL.createObjectURL(blob);
|
|
1364
|
+
link.setAttribute('href', url);
|
|
1365
|
+
link.setAttribute('download', filename);
|
|
1366
|
+
link.style.visibility = 'hidden';
|
|
1367
|
+
document.body.appendChild(link);
|
|
1368
|
+
link.click();
|
|
1580
1369
|
document.body.removeChild(link);
|
|
1581
|
-
URL
|
|
1582
|
-
|
|
1370
|
+
// Освобождение URL
|
|
1371
|
+
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
1583
1372
|
}
|
|
1584
|
-
|
|
1585
1373
|
/**
|
|
1586
|
-
*
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1374
|
+
* Асинхронная версия downloadAsCsv
|
|
1375
|
+
*/
|
|
1376
|
+
async function downloadAsCsvAsync$1(data, filename = 'data.csv', options = {}) {
|
|
1377
|
+
return downloadAsCsv(data, filename, options);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Парсит CSV файл из input[type="file"]
|
|
1381
|
+
*
|
|
1382
|
+
* @param file - File объект из input
|
|
1383
|
+
* @param options - Опции для csvToJson
|
|
1384
|
+
* @returns Promise с распарсенными данными
|
|
1596
1385
|
*/
|
|
1597
1386
|
async function parseCsvFile(file, options = {}) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
throw new ValidationError('Input must be a File object');
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// Проверка расширения файла
|
|
1609
|
-
if (!file.name.toLowerCase().endsWith('.csv')) {
|
|
1610
|
-
throw new ValidationError('File must have .csv extension');
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
// Проверка размера файла (предупреждение для больших файлов)
|
|
1614
|
-
const MAX_SIZE_WARNING = 50 * 1024 * 1024; // 50MB
|
|
1615
|
-
if (file.size > MAX_SIZE_WARNING && process.env.NODE_ENV !== 'production') {
|
|
1616
|
-
console.warn(`⚠️ Warning: Processing large file (${(file.size / 1024 / 1024).toFixed(2)}MB).\n` + '💡 Consider using Web Workers for better performance.\n' + '🔧 Tip: Use parseCSVWithWorker() for files > 10MB.');
|
|
1617
|
-
}
|
|
1618
|
-
return new Promise((resolve, reject) => {
|
|
1619
|
-
const reader = new FileReader();
|
|
1620
|
-
reader.onload = function (event) {
|
|
1621
|
-
try {
|
|
1622
|
-
const csvText = event.target.result;
|
|
1623
|
-
const json = csvToJson(csvText, options);
|
|
1624
|
-
resolve(json);
|
|
1625
|
-
} catch (error) {
|
|
1626
|
-
reject(error);
|
|
1627
|
-
}
|
|
1628
|
-
};
|
|
1629
|
-
reader.onerror = function () {
|
|
1630
|
-
reject(new ValidationError('Ошибка чтения файла'));
|
|
1631
|
-
};
|
|
1632
|
-
reader.onabort = function () {
|
|
1633
|
-
reject(new ValidationError('Чтение файла прервано'));
|
|
1634
|
-
};
|
|
1635
|
-
|
|
1636
|
-
// Чтение как текст
|
|
1637
|
-
reader.readAsText(file, 'UTF-8');
|
|
1638
|
-
});
|
|
1387
|
+
if (!(file instanceof File)) {
|
|
1388
|
+
throw new ValidationError('parseCsvFile() ожидает объект File');
|
|
1389
|
+
}
|
|
1390
|
+
// Чтение файла как текст
|
|
1391
|
+
const text = await file.text();
|
|
1392
|
+
// Парсинг CSV
|
|
1393
|
+
return csvToJson$1(text, options);
|
|
1639
1394
|
}
|
|
1640
|
-
|
|
1641
1395
|
/**
|
|
1642
|
-
*
|
|
1396
|
+
* Парсит CSV файл потоково
|
|
1643
1397
|
*
|
|
1644
|
-
* @param
|
|
1645
|
-
* @param
|
|
1646
|
-
* @returns
|
|
1398
|
+
* @param file - File объект
|
|
1399
|
+
* @param options - Опции для потокового парсинга
|
|
1400
|
+
* @returns AsyncIterator с данными
|
|
1647
1401
|
*/
|
|
1648
1402
|
function parseCsvFileStream(file, options = {}) {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1403
|
+
if (!(file instanceof File)) {
|
|
1404
|
+
throw new ValidationError('parseCsvFileStream() ожидает объект File');
|
|
1405
|
+
}
|
|
1406
|
+
// Используем csvToJsonIterator из импортированного модуля
|
|
1407
|
+
return csvToJsonIterator$1(file, options);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Создает поток для конвертации JSON в CSV
|
|
1411
|
+
*
|
|
1412
|
+
* @param options - Опции для jsonToCsv
|
|
1413
|
+
* @returns ReadableStream
|
|
1414
|
+
*/
|
|
1415
|
+
function jsonToCsvStream(options = {}) {
|
|
1416
|
+
return jsonToCsvStream$1(options);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Создает поток для конвертации JSON в NDJSON
|
|
1420
|
+
*
|
|
1421
|
+
* @param options - Опции для конвертации
|
|
1422
|
+
* @returns ReadableStream
|
|
1423
|
+
*/
|
|
1424
|
+
function jsonToNdjsonStream(options = {}) {
|
|
1425
|
+
return jsonToNdjsonStream$1(options);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Создает поток для парсинга CSV в JSON
|
|
1429
|
+
*
|
|
1430
|
+
* @param options - Опции для csvToJson
|
|
1431
|
+
* @returns ReadableStream
|
|
1432
|
+
*/
|
|
1433
|
+
function csvToJsonStream(options = {}) {
|
|
1434
|
+
return csvToJsonStream$1(options);
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Загружает CSV файл по URL
|
|
1438
|
+
*
|
|
1439
|
+
* @param url - URL CSV файла
|
|
1440
|
+
* @param options - Опции для csvToJson
|
|
1441
|
+
* @returns Promise с распарсенными данными
|
|
1442
|
+
*/
|
|
1443
|
+
async function loadCsvFromUrl(url, options = {}) {
|
|
1444
|
+
if (typeof window === 'undefined') {
|
|
1445
|
+
throw new ValidationError('loadCsvFromUrl() работает только в браузере');
|
|
1446
|
+
}
|
|
1447
|
+
const response = await fetch(url);
|
|
1448
|
+
if (!response.ok) {
|
|
1449
|
+
throw new ValidationError(`Failed to load CSV from URL: ${response.status} ${response.statusText}`);
|
|
1450
|
+
}
|
|
1451
|
+
const text = await response.text();
|
|
1452
|
+
return csvToJson$1(text, options);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Асинхронная версия loadCsvFromUrl
|
|
1456
|
+
*/
|
|
1457
|
+
async function loadCsvFromUrlAsync(url, options = {}) {
|
|
1458
|
+
return loadCsvFromUrl(url, options);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Экспортирует данные в CSV и открывает в новой вкладке
|
|
1462
|
+
*
|
|
1463
|
+
* @param data - Данные для экспорта
|
|
1464
|
+
* @param options - Опции для jsonToCsv
|
|
1465
|
+
*/
|
|
1466
|
+
function openCsvInNewTab(data, options = {}) {
|
|
1467
|
+
if (typeof window === 'undefined') {
|
|
1468
|
+
throw new ValidationError('openCsvInNewTab() работает только в браузере');
|
|
1469
|
+
}
|
|
1470
|
+
const csv = jsonToCsv$1(data, options);
|
|
1471
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
1472
|
+
const url = URL.createObjectURL(blob);
|
|
1473
|
+
window.open(url, '_blank');
|
|
1474
|
+
// Освобождение URL через некоторое время
|
|
1475
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Асинхронная версия openCsvInNewTab
|
|
1479
|
+
*/
|
|
1480
|
+
async function openCsvInNewTabAsync(data, options = {}) {
|
|
1481
|
+
return openCsvInNewTab(data, options);
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Копирует CSV в буфер обмена
|
|
1485
|
+
*
|
|
1486
|
+
* @param data - Данные для копирования
|
|
1487
|
+
* @param options - Опции для jsonToCsv
|
|
1488
|
+
* @returns Promise с результатом копирования
|
|
1489
|
+
*/
|
|
1490
|
+
async function copyCsvToClipboard(data, options = {}) {
|
|
1491
|
+
if (typeof window === 'undefined' || !navigator.clipboard) {
|
|
1492
|
+
throw new ValidationError('copyCsvToClipboard() требует поддержки Clipboard API');
|
|
1493
|
+
}
|
|
1494
|
+
const csv = jsonToCsv$1(data, options);
|
|
1495
|
+
try {
|
|
1496
|
+
await navigator.clipboard.writeText(csv);
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
console.error('Failed to copy to clipboard:', error);
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Сохраняет CSV в localStorage
|
|
1506
|
+
*
|
|
1507
|
+
* @param key - Ключ для сохранения
|
|
1508
|
+
* @param data - Данные для сохранения
|
|
1509
|
+
* @param options - Опции для jsonToCsv
|
|
1510
|
+
*/
|
|
1511
|
+
function saveCsvToLocalStorage(key, data, options = {}) {
|
|
1512
|
+
if (typeof window === 'undefined' || !localStorage) {
|
|
1513
|
+
throw new ValidationError('saveCsvToLocalStorage() требует localStorage');
|
|
1514
|
+
}
|
|
1515
|
+
const csv = jsonToCsv$1(data, options);
|
|
1516
|
+
localStorage.setItem(key, csv);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Загружает CSV из localStorage
|
|
1520
|
+
*
|
|
1521
|
+
* @param key - Ключ для загрузки
|
|
1522
|
+
* @param options - Опции для csvToJson
|
|
1523
|
+
* @returns Распарсенные данные или null
|
|
1524
|
+
*/
|
|
1525
|
+
function loadCsvFromLocalStorage(key, options = {}) {
|
|
1526
|
+
if (typeof window === 'undefined' || !localStorage) {
|
|
1527
|
+
throw new ValidationError('loadCsvFromLocalStorage() требует localStorage');
|
|
1528
|
+
}
|
|
1529
|
+
const csv = localStorage.getItem(key);
|
|
1530
|
+
if (!csv) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
return csvToJson$1(csv, options);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Асинхронная версия loadCsvFromLocalStorage
|
|
1537
|
+
*/
|
|
1538
|
+
async function loadCsvFromLocalStorageAsync(key, options = {}) {
|
|
1539
|
+
return loadCsvFromLocalStorage(key, options);
|
|
1659
1540
|
}
|
|
1660
|
-
|
|
1661
1541
|
/**
|
|
1662
1542
|
* Создает CSV файл из JSON данных (альтернатива downloadAsCsv)
|
|
1663
1543
|
* Возвращает Blob вместо автоматического скачивания
|
|
1664
|
-
*
|
|
1665
|
-
* @param
|
|
1666
|
-
* @param
|
|
1667
|
-
* @returns
|
|
1544
|
+
*
|
|
1545
|
+
* @param data - Массив объектов
|
|
1546
|
+
* @param options - Опции для jsonToCsv
|
|
1547
|
+
* @returns CSV Blob
|
|
1668
1548
|
*/
|
|
1669
1549
|
function createCsvBlob(data, options = {}) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1550
|
+
const csv = jsonToCsv$1(data, options);
|
|
1551
|
+
return new Blob([csv], {
|
|
1552
|
+
type: 'text/csv;charset=utf-8;'
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Асинхронная версия createCsvBlob
|
|
1557
|
+
*/
|
|
1558
|
+
async function createCsvBlobAsync(data, options = {}) {
|
|
1559
|
+
return createCsvBlob(data, options);
|
|
1674
1560
|
}
|
|
1675
|
-
|
|
1676
1561
|
/**
|
|
1677
1562
|
* Парсит CSV строку из Blob
|
|
1678
|
-
*
|
|
1679
|
-
* @param
|
|
1680
|
-
* @param
|
|
1681
|
-
* @returns
|
|
1563
|
+
*
|
|
1564
|
+
* @param blob - CSV Blob
|
|
1565
|
+
* @param options - Опции для csvToJson
|
|
1566
|
+
* @returns Promise с JSON данными
|
|
1682
1567
|
*/
|
|
1683
1568
|
async function parseCsvBlob(blob, options = {}) {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1569
|
+
if (!(blob instanceof Blob)) {
|
|
1570
|
+
throw new ValidationError('Input must be a Blob object');
|
|
1571
|
+
}
|
|
1572
|
+
return new Promise((resolve, reject) => {
|
|
1573
|
+
const reader = new FileReader();
|
|
1574
|
+
reader.onload = function (event) {
|
|
1575
|
+
try {
|
|
1576
|
+
const csvText = event.target?.result;
|
|
1577
|
+
const json = csvToJson$1(csvText, options);
|
|
1578
|
+
resolve(json);
|
|
1579
|
+
}
|
|
1580
|
+
catch (error) {
|
|
1581
|
+
reject(error);
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
reader.onerror = function () {
|
|
1585
|
+
reject(new ValidationError('Ошибка чтения Blob'));
|
|
1586
|
+
};
|
|
1587
|
+
reader.readAsText(blob, 'UTF-8');
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Асинхронная версия parseCsvBlob
|
|
1592
|
+
*/
|
|
1593
|
+
async function parseCsvBlobAsync(blob, options = {}) {
|
|
1594
|
+
return parseCsvBlob(blob, options);
|
|
1703
1595
|
}
|
|
1704
|
-
|
|
1705
1596
|
// Экспорт для Node.js совместимости
|
|
1706
1597
|
if (typeof module !== 'undefined' && module.exports) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1598
|
+
module.exports = {
|
|
1599
|
+
downloadAsCsv,
|
|
1600
|
+
downloadAsCsvAsync: downloadAsCsvAsync$1,
|
|
1601
|
+
parseCsvFile,
|
|
1602
|
+
parseCsvFileStream,
|
|
1603
|
+
createCsvBlob,
|
|
1604
|
+
createCsvBlobAsync,
|
|
1605
|
+
parseCsvBlob,
|
|
1606
|
+
parseCsvBlobAsync,
|
|
1607
|
+
jsonToCsvStream,
|
|
1608
|
+
jsonToNdjsonStream,
|
|
1609
|
+
csvToJsonStream,
|
|
1610
|
+
loadCsvFromUrl,
|
|
1611
|
+
loadCsvFromUrlAsync,
|
|
1612
|
+
openCsvInNewTab,
|
|
1613
|
+
openCsvInNewTabAsync,
|
|
1614
|
+
copyCsvToClipboard,
|
|
1615
|
+
saveCsvToLocalStorage,
|
|
1616
|
+
loadCsvFromLocalStorage,
|
|
1617
|
+
loadCsvFromLocalStorageAsync
|
|
1618
|
+
};
|
|
1717
1619
|
}
|
|
1718
1620
|
|
|
1719
1621
|
// Worker Pool для параллельной обработки CSV
|
|
1720
1622
|
// Использует Comlink для простой коммуникации с Web Workers
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
1623
|
// Проверка поддержки Web Workers
|
|
1724
1624
|
const WORKERS_SUPPORTED = typeof Worker !== 'undefined';
|
|
1725
1625
|
function isTransferableBuffer(value) {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
}
|
|
1729
|
-
if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer) {
|
|
1730
|
-
return false;
|
|
1731
|
-
}
|
|
1732
|
-
return true;
|
|
1733
|
-
}
|
|
1734
|
-
function collectTransferables(args) {
|
|
1735
|
-
const transferables = [];
|
|
1736
|
-
const collectFromValue = value => {
|
|
1737
|
-
if (!value) {
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
if (isTransferableBuffer(value)) {
|
|
1741
|
-
transferables.push(value);
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
if (ArrayBuffer.isView(value) && isTransferableBuffer(value.buffer)) {
|
|
1745
|
-
transferables.push(value.buffer);
|
|
1746
|
-
return;
|
|
1626
|
+
if (!(value instanceof ArrayBuffer)) {
|
|
1627
|
+
return false;
|
|
1747
1628
|
}
|
|
1748
|
-
if (
|
|
1749
|
-
|
|
1629
|
+
if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer) {
|
|
1630
|
+
return false;
|
|
1750
1631
|
}
|
|
1751
|
-
|
|
1752
|
-
args.forEach(collectFromValue);
|
|
1753
|
-
return transferables.length ? transferables : null;
|
|
1632
|
+
return true;
|
|
1754
1633
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1634
|
+
function collectTransferables(args) {
|
|
1635
|
+
const transferables = [];
|
|
1636
|
+
const collectFromValue = (value) => {
|
|
1637
|
+
if (!value) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (isTransferableBuffer(value)) {
|
|
1641
|
+
transferables.push(value);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (ArrayBuffer.isView(value) && isTransferableBuffer(value.buffer)) {
|
|
1645
|
+
transferables.push(value.buffer);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (Array.isArray(value)) {
|
|
1649
|
+
value.forEach(collectFromValue);
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
args.forEach(collectFromValue);
|
|
1653
|
+
return transferables.length ? transferables : null;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Опции для Worker Pool
|
|
1657
|
+
* @typedef {Object} WorkerPoolOptions
|
|
1658
|
+
* @property {number} [workerCount=4] - Количество workers в pool
|
|
1659
|
+
* @property {number} [maxQueueSize=100] - Максимальный размер очереди задач
|
|
1660
|
+
* @property {boolean} [autoScale=true] - Автоматическое масштабирование pool
|
|
1661
|
+
* @property {number} [idleTimeout=60000] - Таймаут простоя worker (мс)
|
|
1763
1662
|
*/
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
*
|
|
1767
|
-
* @
|
|
1768
|
-
* @property {number}
|
|
1769
|
-
* @property {number}
|
|
1770
|
-
* @property {number}
|
|
1771
|
-
* @property {number}
|
|
1772
|
-
* @property {number}
|
|
1773
|
-
* @property {number} tasksFailed - Неудачные задачи
|
|
1663
|
+
/**
|
|
1664
|
+
* Статистика Worker Pool
|
|
1665
|
+
* @typedef {Object} WorkerPoolStats
|
|
1666
|
+
* @property {number} totalWorkers - Всего workers
|
|
1667
|
+
* @property {number} activeWorkers - Активные workers
|
|
1668
|
+
* @property {number} idleWorkers - Простаивающие workers
|
|
1669
|
+
* @property {number} queueSize - Размер очереди
|
|
1670
|
+
* @property {number} tasksCompleted - Завершенные задачи
|
|
1671
|
+
* @property {number} tasksFailed - Неудачные задачи
|
|
1774
1672
|
*/
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
*
|
|
1778
|
-
* @
|
|
1779
|
-
* @property {number}
|
|
1780
|
-
* @property {number}
|
|
1781
|
-
* @property {number}
|
|
1782
|
-
* @property {number} speed - Скорость обработки (элементов/сек)
|
|
1673
|
+
/**
|
|
1674
|
+
* Прогресс обработки задачи
|
|
1675
|
+
* @typedef {Object} TaskProgress
|
|
1676
|
+
* @property {number} processed - Обработано элементов
|
|
1677
|
+
* @property {number} total - Всего элементов
|
|
1678
|
+
* @property {number} percentage - Процент выполнения
|
|
1679
|
+
* @property {number} speed - Скорость обработки (элементов/сек)
|
|
1783
1680
|
*/
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
* Worker Pool для параллельной обработки CSV
|
|
1681
|
+
/**
|
|
1682
|
+
* Worker Pool для параллельной обработки CSV
|
|
1787
1683
|
*/
|
|
1788
1684
|
class WorkerPool {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
/**
|
|
1821
|
-
* Инициализация workers
|
|
1822
|
-
* @private
|
|
1823
|
-
*/
|
|
1824
|
-
initializeWorkers() {
|
|
1825
|
-
const {
|
|
1826
|
-
workerCount
|
|
1827
|
-
} = this.options;
|
|
1828
|
-
for (let i = 0; i < workerCount; i++) {
|
|
1829
|
-
this.createWorker();
|
|
1830
|
-
}
|
|
1831
|
-
this.updateStats();
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
/**
|
|
1835
|
-
* Создает нового worker
|
|
1836
|
-
* @private
|
|
1837
|
-
*/
|
|
1838
|
-
createWorker() {
|
|
1839
|
-
try {
|
|
1840
|
-
const worker = new Worker(this.workerScript, {
|
|
1841
|
-
type: 'module'
|
|
1842
|
-
});
|
|
1843
|
-
worker.id = `worker-${this.workers.length}`;
|
|
1844
|
-
worker.status = 'idle';
|
|
1845
|
-
worker.lastUsed = Date.now();
|
|
1846
|
-
worker.taskId = null;
|
|
1847
|
-
|
|
1848
|
-
// Обработчики событий
|
|
1849
|
-
worker.onmessage = event => this.handleWorkerMessage(worker, event);
|
|
1850
|
-
worker.onerror = error => this.handleWorkerError(worker, error);
|
|
1851
|
-
worker.onmessageerror = error => this.handleWorkerMessageError(worker, error);
|
|
1852
|
-
this.workers.push(worker);
|
|
1853
|
-
this.stats.totalWorkers++;
|
|
1854
|
-
this.stats.idleWorkers++;
|
|
1855
|
-
return worker;
|
|
1856
|
-
} catch (error) {
|
|
1857
|
-
throw new ConfigurationError(`Не удалось создать worker: ${error.message}`);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
/**
|
|
1862
|
-
* Обработка сообщений от worker
|
|
1863
|
-
* @private
|
|
1864
|
-
*/
|
|
1865
|
-
handleWorkerMessage(worker, event) {
|
|
1866
|
-
const {
|
|
1867
|
-
data
|
|
1868
|
-
} = event;
|
|
1869
|
-
if (data.type === 'PROGRESS') {
|
|
1870
|
-
this.handleProgress(worker, data);
|
|
1871
|
-
} else if (data.type === 'RESULT') {
|
|
1872
|
-
this.handleResult(worker, data);
|
|
1873
|
-
} else if (data.type === 'ERROR') {
|
|
1874
|
-
this.handleWorkerTaskError(worker, data);
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* Обработка прогресса задачи
|
|
1880
|
-
* @private
|
|
1881
|
-
*/
|
|
1882
|
-
handleProgress(worker, progressData) {
|
|
1883
|
-
const taskId = worker.taskId;
|
|
1884
|
-
if (taskId && this.activeTasks.has(taskId)) {
|
|
1885
|
-
const task = this.activeTasks.get(taskId);
|
|
1886
|
-
if (task.onProgress) {
|
|
1887
|
-
task.onProgress({
|
|
1888
|
-
processed: progressData.processed,
|
|
1889
|
-
total: progressData.total,
|
|
1890
|
-
percentage: progressData.processed / progressData.total * 100,
|
|
1891
|
-
speed: progressData.speed || 0
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Создает новый Worker Pool
|
|
1687
|
+
* @param {string} workerScript - URL скрипта worker
|
|
1688
|
+
* @param {WorkerPoolOptions} [options] - Опции pool
|
|
1689
|
+
*/
|
|
1690
|
+
constructor(workerScript, options = {}) {
|
|
1691
|
+
if (!WORKERS_SUPPORTED) {
|
|
1692
|
+
throw new ValidationError('Web Workers не поддерживаются в этом браузере');
|
|
1693
|
+
}
|
|
1694
|
+
this.workerScript = workerScript;
|
|
1695
|
+
this.options = {
|
|
1696
|
+
workerCount: 4,
|
|
1697
|
+
maxQueueSize: 100,
|
|
1698
|
+
autoScale: true,
|
|
1699
|
+
idleTimeout: 60000,
|
|
1700
|
+
...options
|
|
1701
|
+
};
|
|
1702
|
+
this.workers = [];
|
|
1703
|
+
this.taskQueue = [];
|
|
1704
|
+
this.activeTasks = new Map();
|
|
1705
|
+
this.stats = {
|
|
1706
|
+
totalWorkers: 0,
|
|
1707
|
+
activeWorkers: 0,
|
|
1708
|
+
idleWorkers: 0,
|
|
1709
|
+
queueSize: 0,
|
|
1710
|
+
tasksCompleted: 0,
|
|
1711
|
+
tasksFailed: 0
|
|
1712
|
+
};
|
|
1713
|
+
this.initializeWorkers();
|
|
1894
1714
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
// Освобождение worker
|
|
1907
|
-
worker.status = 'idle';
|
|
1908
|
-
worker.lastUsed = Date.now();
|
|
1909
|
-
worker.taskId = null;
|
|
1910
|
-
this.stats.activeWorkers--;
|
|
1911
|
-
this.stats.idleWorkers++;
|
|
1912
|
-
|
|
1913
|
-
// Завершение задачи
|
|
1914
|
-
task.resolve(resultData.data);
|
|
1915
|
-
this.activeTasks.delete(taskId);
|
|
1916
|
-
this.stats.tasksCompleted++;
|
|
1917
|
-
|
|
1918
|
-
// Обработка следующей задачи в очереди
|
|
1919
|
-
this.processQueue();
|
|
1920
|
-
this.updateStats();
|
|
1715
|
+
/**
|
|
1716
|
+
* Инициализация workers
|
|
1717
|
+
* @private
|
|
1718
|
+
*/
|
|
1719
|
+
initializeWorkers() {
|
|
1720
|
+
const { workerCount } = this.options;
|
|
1721
|
+
for (let i = 0; i < workerCount; i++) {
|
|
1722
|
+
this.createWorker();
|
|
1723
|
+
}
|
|
1724
|
+
this.updateStats();
|
|
1921
1725
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
if (errorData.details) {
|
|
1946
|
-
workerError.details = errorData.details;
|
|
1947
|
-
}
|
|
1948
|
-
task.reject(workerError);
|
|
1949
|
-
this.activeTasks.delete(taskId);
|
|
1950
|
-
this.stats.tasksFailed++;
|
|
1951
|
-
|
|
1952
|
-
// Обработка следующей задачи
|
|
1953
|
-
this.processQueue();
|
|
1954
|
-
this.updateStats();
|
|
1726
|
+
/**
|
|
1727
|
+
* Создает нового worker
|
|
1728
|
+
* @private
|
|
1729
|
+
*/
|
|
1730
|
+
createWorker() {
|
|
1731
|
+
try {
|
|
1732
|
+
const worker = new Worker(this.workerScript, { type: 'module' });
|
|
1733
|
+
worker.id = `worker-${this.workers.length}`;
|
|
1734
|
+
worker.status = 'idle';
|
|
1735
|
+
worker.lastUsed = Date.now();
|
|
1736
|
+
worker.taskId = null;
|
|
1737
|
+
// Обработчики событий
|
|
1738
|
+
worker.onmessage = (event) => this.handleWorkerMessage(worker, event);
|
|
1739
|
+
worker.onerror = (error) => this.handleWorkerError(worker, error);
|
|
1740
|
+
worker.onmessageerror = (error) => this.handleWorkerMessageError(worker, error);
|
|
1741
|
+
this.workers.push(worker);
|
|
1742
|
+
this.stats.totalWorkers++;
|
|
1743
|
+
this.stats.idleWorkers++;
|
|
1744
|
+
return worker;
|
|
1745
|
+
}
|
|
1746
|
+
catch (error) {
|
|
1747
|
+
throw new ConfigurationError(`Не удалось создать worker: ${error.message}`);
|
|
1748
|
+
}
|
|
1955
1749
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1750
|
+
/**
|
|
1751
|
+
* Обработка сообщений от worker
|
|
1752
|
+
* @private
|
|
1753
|
+
*/
|
|
1754
|
+
handleWorkerMessage(worker, event) {
|
|
1755
|
+
const { data } = event;
|
|
1756
|
+
if (data.type === 'PROGRESS') {
|
|
1757
|
+
this.handleProgress(worker, data);
|
|
1758
|
+
}
|
|
1759
|
+
else if (data.type === 'RESULT') {
|
|
1760
|
+
this.handleResult(worker, data);
|
|
1761
|
+
}
|
|
1762
|
+
else if (data.type === 'ERROR') {
|
|
1763
|
+
this.handleWorkerTaskError(worker, data);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Обработка прогресса задачи
|
|
1768
|
+
* @private
|
|
1769
|
+
*/
|
|
1770
|
+
handleProgress(worker, progressData) {
|
|
1771
|
+
const taskId = worker.taskId;
|
|
1772
|
+
if (taskId && this.activeTasks.has(taskId)) {
|
|
1773
|
+
const task = this.activeTasks.get(taskId);
|
|
1774
|
+
if (task.onProgress) {
|
|
1775
|
+
task.onProgress({
|
|
1776
|
+
processed: progressData.processed,
|
|
1777
|
+
total: progressData.total,
|
|
1778
|
+
percentage: (progressData.processed / progressData.total) * 100,
|
|
1779
|
+
speed: progressData.speed || 0
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Обработка результата задачи
|
|
1786
|
+
* @private
|
|
1787
|
+
*/
|
|
1788
|
+
handleResult(worker, resultData) {
|
|
1789
|
+
const taskId = worker.taskId;
|
|
1790
|
+
if (taskId && this.activeTasks.has(taskId)) {
|
|
1791
|
+
const task = this.activeTasks.get(taskId);
|
|
1792
|
+
// Освобождение worker
|
|
1793
|
+
worker.status = 'idle';
|
|
1794
|
+
worker.lastUsed = Date.now();
|
|
1795
|
+
worker.taskId = null;
|
|
1796
|
+
this.stats.activeWorkers--;
|
|
1797
|
+
this.stats.idleWorkers++;
|
|
1798
|
+
// Завершение задачи
|
|
1799
|
+
task.resolve(resultData.data);
|
|
1800
|
+
this.activeTasks.delete(taskId);
|
|
1801
|
+
this.stats.tasksCompleted++;
|
|
1802
|
+
// Обработка следующей задачи в очереди
|
|
1803
|
+
this.processQueue();
|
|
1804
|
+
this.updateStats();
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Обработка ошибки задачи
|
|
1809
|
+
* @private
|
|
1810
|
+
*/
|
|
1811
|
+
handleWorkerTaskError(worker, errorData) {
|
|
1812
|
+
const taskId = worker.taskId;
|
|
1813
|
+
if (taskId && this.activeTasks.has(taskId)) {
|
|
1814
|
+
const task = this.activeTasks.get(taskId);
|
|
1815
|
+
// Освобождение worker
|
|
1816
|
+
worker.status = 'idle';
|
|
1817
|
+
worker.lastUsed = Date.now();
|
|
1818
|
+
worker.taskId = null;
|
|
1819
|
+
this.stats.activeWorkers--;
|
|
1820
|
+
this.stats.idleWorkers++;
|
|
1821
|
+
// Завершение с ошибкой
|
|
1822
|
+
const workerError = new Error(errorData.message || 'Ошибка в worker');
|
|
1823
|
+
if (errorData.code) {
|
|
1824
|
+
workerError.code = errorData.code;
|
|
1825
|
+
}
|
|
1826
|
+
if (errorData.details) {
|
|
1827
|
+
workerError.details = errorData.details;
|
|
1828
|
+
}
|
|
1829
|
+
task.reject(workerError);
|
|
1830
|
+
this.activeTasks.delete(taskId);
|
|
1831
|
+
this.stats.tasksFailed++;
|
|
1832
|
+
// Обработка следующей задачи
|
|
1833
|
+
this.processQueue();
|
|
1834
|
+
this.updateStats();
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Обработка ошибок worker
|
|
1839
|
+
* @private
|
|
1840
|
+
*/
|
|
1841
|
+
handleWorkerError(worker, error) {
|
|
1842
|
+
console.error(`Worker ${worker.id} error:`, error);
|
|
1843
|
+
// Перезапуск worker
|
|
1844
|
+
this.restartWorker(worker);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Обработка ошибок сообщений
|
|
1848
|
+
* @private
|
|
1849
|
+
*/
|
|
1850
|
+
handleWorkerMessageError(worker, error) {
|
|
1851
|
+
console.error(`Worker ${worker.id} message error:`, error);
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Перезапуск worker
|
|
1855
|
+
* @private
|
|
1856
|
+
*/
|
|
1857
|
+
restartWorker(worker) {
|
|
1858
|
+
const index = this.workers.indexOf(worker);
|
|
1859
|
+
if (index !== -1) {
|
|
1860
|
+
// Завершение старого worker
|
|
1861
|
+
worker.terminate();
|
|
1862
|
+
// Удаление из статистики
|
|
1863
|
+
if (worker.status === 'active') {
|
|
1864
|
+
this.stats.activeWorkers--;
|
|
1865
|
+
}
|
|
1866
|
+
else {
|
|
1867
|
+
this.stats.idleWorkers--;
|
|
1868
|
+
}
|
|
1869
|
+
this.stats.totalWorkers--;
|
|
1870
|
+
// Создание нового worker
|
|
1871
|
+
const newWorker = this.createWorker();
|
|
1872
|
+
this.workers[index] = newWorker;
|
|
1873
|
+
// Перезапуск задачи если была активна
|
|
1874
|
+
if (worker.taskId && this.activeTasks.has(worker.taskId)) {
|
|
1875
|
+
const task = this.activeTasks.get(worker.taskId);
|
|
1876
|
+
this.executeTask(newWorker, task);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Выполнение задачи на worker
|
|
1882
|
+
* @private
|
|
1883
|
+
*/
|
|
1884
|
+
executeTask(worker, task) {
|
|
1885
|
+
worker.status = 'active';
|
|
1886
|
+
worker.lastUsed = Date.now();
|
|
1887
|
+
worker.taskId = task.id;
|
|
1991
1888
|
this.stats.idleWorkers--;
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1889
|
+
this.stats.activeWorkers++;
|
|
1890
|
+
// Отправка задачи в worker
|
|
1891
|
+
const payload = {
|
|
1892
|
+
type: 'EXECUTE',
|
|
1893
|
+
taskId: task.id,
|
|
1894
|
+
method: task.method,
|
|
1895
|
+
args: task.args,
|
|
1896
|
+
options: task.options
|
|
1897
|
+
};
|
|
1898
|
+
if (task.transferList && task.transferList.length) {
|
|
1899
|
+
worker.postMessage(payload, task.transferList);
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
worker.postMessage(payload);
|
|
1903
|
+
}
|
|
2004
1904
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
1905
|
+
/**
|
|
1906
|
+
* Обработка очереди задач
|
|
1907
|
+
* @private
|
|
1908
|
+
*/
|
|
1909
|
+
processQueue() {
|
|
1910
|
+
if (this.taskQueue.length === 0) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
while (this.taskQueue.length > 0) {
|
|
1914
|
+
const idleWorker = this.workers.find(w => w.status === 'idle');
|
|
1915
|
+
if (!idleWorker) {
|
|
1916
|
+
if (this.options.autoScale && this.workers.length < this.options.maxQueueSize) {
|
|
1917
|
+
this.createWorker();
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
const task = this.taskQueue.shift();
|
|
1923
|
+
this.stats.queueSize--;
|
|
1924
|
+
this.executeTask(idleWorker, task);
|
|
1925
|
+
}
|
|
1926
|
+
this.updateStats();
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Обновление статистики
|
|
1930
|
+
* @private
|
|
1931
|
+
*/
|
|
1932
|
+
updateStats() {
|
|
1933
|
+
this.stats.queueSize = this.taskQueue.length;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Выполнение задачи через pool
|
|
1937
|
+
* @param {string} method - Метод для вызова в worker
|
|
1938
|
+
* @param {Array} args - Аргументы метода
|
|
1939
|
+
* @param {Object} [options] - Опции задачи
|
|
1940
|
+
* @param {Function} [onProgress] - Callback прогресса
|
|
1941
|
+
* @returns {Promise<unknown>} Результат выполнения
|
|
1942
|
+
*/
|
|
1943
|
+
async exec(method, args = [], options = {}, onProgress = null) {
|
|
1944
|
+
return new Promise((resolve, reject) => {
|
|
1945
|
+
// Проверка размера очереди
|
|
1946
|
+
if (this.taskQueue.length >= this.options.maxQueueSize) {
|
|
1947
|
+
reject(new Error('Очередь задач переполнена'));
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
// Создание задачи
|
|
1951
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1952
|
+
const { transfer, ...taskOptions } = options || {};
|
|
1953
|
+
const transferList = transfer || collectTransferables(args);
|
|
1954
|
+
const task = {
|
|
1955
|
+
id: taskId,
|
|
1956
|
+
method,
|
|
1957
|
+
args,
|
|
1958
|
+
options: taskOptions,
|
|
1959
|
+
transferList,
|
|
1960
|
+
onProgress,
|
|
1961
|
+
resolve,
|
|
1962
|
+
reject,
|
|
1963
|
+
createdAt: Date.now()
|
|
1964
|
+
};
|
|
1965
|
+
// Добавление в очередь
|
|
1966
|
+
this.taskQueue.push(task);
|
|
1967
|
+
this.stats.queueSize++;
|
|
1968
|
+
// Запуск обработки очереди
|
|
1969
|
+
this.processQueue();
|
|
1970
|
+
this.updateStats();
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Получение статистики pool
|
|
1975
|
+
* @returns {WorkerPoolStats} Статистика
|
|
1976
|
+
*/
|
|
1977
|
+
getStats() {
|
|
1978
|
+
return { ...this.stats };
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Очистка простаивающих workers
|
|
1982
|
+
*/
|
|
1983
|
+
cleanupIdleWorkers() {
|
|
1984
|
+
const now = Date.now();
|
|
1985
|
+
const { idleTimeout } = this.options;
|
|
1986
|
+
for (let i = this.workers.length - 1; i >= 0; i--) {
|
|
1987
|
+
const worker = this.workers[i];
|
|
1988
|
+
if (worker.status === 'idle' && (now - worker.lastUsed) > idleTimeout) {
|
|
1989
|
+
// Сохранение минимального количества workers
|
|
1990
|
+
if (this.workers.length > 1) {
|
|
1991
|
+
worker.terminate();
|
|
1992
|
+
this.workers.splice(i, 1);
|
|
1993
|
+
this.stats.totalWorkers--;
|
|
1994
|
+
this.stats.idleWorkers--;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Завершение всех workers
|
|
2001
|
+
*/
|
|
2002
|
+
terminate() {
|
|
2003
|
+
this.workers.forEach(worker => {
|
|
2004
|
+
worker.terminate();
|
|
2005
|
+
});
|
|
2006
|
+
this.workers = [];
|
|
2007
|
+
this.taskQueue = [];
|
|
2008
|
+
this.activeTasks.clear();
|
|
2009
|
+
// Сброс статистики
|
|
2010
|
+
this.stats = {
|
|
2011
|
+
totalWorkers: 0,
|
|
2012
|
+
activeWorkers: 0,
|
|
2013
|
+
idleWorkers: 0,
|
|
2014
|
+
queueSize: 0,
|
|
2015
|
+
tasksCompleted: 0,
|
|
2016
|
+
tasksFailed: 0
|
|
2017
|
+
};
|
|
2030
2018
|
}
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
/**
|
|
2034
|
-
* Обработка очереди задач
|
|
2035
|
-
* @private
|
|
2036
|
-
*/
|
|
2037
|
-
processQueue() {
|
|
2038
|
-
if (this.taskQueue.length === 0) {
|
|
2039
|
-
return;
|
|
2040
|
-
}
|
|
2041
|
-
while (this.taskQueue.length > 0) {
|
|
2042
|
-
const idleWorker = this.workers.find(w => w.status === 'idle');
|
|
2043
|
-
if (!idleWorker) {
|
|
2044
|
-
if (this.options.autoScale && this.workers.length < this.options.maxQueueSize) {
|
|
2045
|
-
this.createWorker();
|
|
2046
|
-
continue;
|
|
2047
|
-
}
|
|
2048
|
-
break;
|
|
2049
|
-
}
|
|
2050
|
-
const task = this.taskQueue.shift();
|
|
2051
|
-
this.stats.queueSize--;
|
|
2052
|
-
this.executeTask(idleWorker, task);
|
|
2053
|
-
}
|
|
2054
|
-
this.updateStats();
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
/**
|
|
2058
|
-
* Обновление статистики
|
|
2059
|
-
* @private
|
|
2060
|
-
*/
|
|
2061
|
-
updateStats() {
|
|
2062
|
-
this.stats.queueSize = this.taskQueue.length;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
/**
|
|
2066
|
-
* Выполнение задачи через pool
|
|
2067
|
-
* @param {string} method - Метод для вызова в worker
|
|
2068
|
-
* @param {Array} args - Аргументы метода
|
|
2069
|
-
* @param {Object} [options] - Опции задачи
|
|
2070
|
-
* @param {Function} [onProgress] - Callback прогресса
|
|
2071
|
-
* @returns {Promise<any>} Результат выполнения
|
|
2072
|
-
*/
|
|
2073
|
-
async exec(method, args = [], options = {}, onProgress = null) {
|
|
2074
|
-
return new Promise((resolve, reject) => {
|
|
2075
|
-
// Проверка размера очереди
|
|
2076
|
-
if (this.taskQueue.length >= this.options.maxQueueSize) {
|
|
2077
|
-
reject(new Error('Очередь задач переполнена'));
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
// Создание задачи
|
|
2082
|
-
const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2083
|
-
const {
|
|
2084
|
-
transfer,
|
|
2085
|
-
...taskOptions
|
|
2086
|
-
} = options || {};
|
|
2087
|
-
const transferList = transfer || collectTransferables(args);
|
|
2088
|
-
const task = {
|
|
2089
|
-
id: taskId,
|
|
2090
|
-
method,
|
|
2091
|
-
args,
|
|
2092
|
-
options: taskOptions,
|
|
2093
|
-
transferList,
|
|
2094
|
-
onProgress,
|
|
2095
|
-
resolve,
|
|
2096
|
-
reject,
|
|
2097
|
-
createdAt: Date.now()
|
|
2098
|
-
};
|
|
2099
|
-
|
|
2100
|
-
// Добавление в очередь
|
|
2101
|
-
this.taskQueue.push(task);
|
|
2102
|
-
this.stats.queueSize++;
|
|
2103
|
-
|
|
2104
|
-
// Запуск обработки очереди
|
|
2105
|
-
this.processQueue();
|
|
2106
|
-
this.updateStats();
|
|
2107
|
-
});
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
/**
|
|
2111
|
-
* Получение статистики pool
|
|
2112
|
-
* @returns {WorkerPoolStats} Статистика
|
|
2113
|
-
*/
|
|
2114
|
-
getStats() {
|
|
2115
|
-
return {
|
|
2116
|
-
...this.stats
|
|
2117
|
-
};
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
/**
|
|
2121
|
-
* Очистка простаивающих workers
|
|
2122
|
-
*/
|
|
2123
|
-
cleanupIdleWorkers() {
|
|
2124
|
-
const now = Date.now();
|
|
2125
|
-
const {
|
|
2126
|
-
idleTimeout
|
|
2127
|
-
} = this.options;
|
|
2128
|
-
for (let i = this.workers.length - 1; i >= 0; i--) {
|
|
2129
|
-
const worker = this.workers[i];
|
|
2130
|
-
if (worker.status === 'idle' && now - worker.lastUsed > idleTimeout) {
|
|
2131
|
-
// Сохранение минимального количества workers
|
|
2132
|
-
if (this.workers.length > 1) {
|
|
2133
|
-
worker.terminate();
|
|
2134
|
-
this.workers.splice(i, 1);
|
|
2135
|
-
this.stats.totalWorkers--;
|
|
2136
|
-
this.stats.idleWorkers--;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
/**
|
|
2143
|
-
* Завершение всех workers
|
|
2144
|
-
*/
|
|
2145
|
-
terminate() {
|
|
2146
|
-
this.workers.forEach(worker => {
|
|
2147
|
-
worker.terminate();
|
|
2148
|
-
});
|
|
2149
|
-
this.workers = [];
|
|
2150
|
-
this.taskQueue = [];
|
|
2151
|
-
this.activeTasks.clear();
|
|
2152
|
-
|
|
2153
|
-
// Сброс статистики
|
|
2154
|
-
this.stats = {
|
|
2155
|
-
totalWorkers: 0,
|
|
2156
|
-
activeWorkers: 0,
|
|
2157
|
-
idleWorkers: 0,
|
|
2158
|
-
queueSize: 0,
|
|
2159
|
-
tasksCompleted: 0,
|
|
2160
|
-
tasksFailed: 0
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
2019
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
*
|
|
2167
|
-
* @
|
|
2168
|
-
* @returns {WorkerPool} Worker Pool
|
|
2020
|
+
/**
|
|
2021
|
+
* Создает Worker Pool для обработки CSV
|
|
2022
|
+
* @param {WorkerPoolOptions} [options] - Опции pool
|
|
2023
|
+
* @returns {WorkerPool} Worker Pool
|
|
2169
2024
|
*/
|
|
2170
2025
|
function createWorkerPool(options = {}) {
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2026
|
+
// Используем встроенный worker скрипт
|
|
2027
|
+
const baseUrl = typeof document !== 'undefined'
|
|
2028
|
+
? document.baseURI
|
|
2029
|
+
: (typeof self !== 'undefined' && self.location
|
|
2030
|
+
? self.location.href
|
|
2031
|
+
: '');
|
|
2032
|
+
const workerScript = new URL('./csv-parser.worker.js', baseUrl).href;
|
|
2033
|
+
return new WorkerPool(workerScript, options);
|
|
2174
2034
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
*
|
|
2178
|
-
* @param {
|
|
2179
|
-
* @param {
|
|
2180
|
-
* @
|
|
2181
|
-
* @returns {Promise<Array<Object>>} JSON данные
|
|
2035
|
+
/**
|
|
2036
|
+
* Парсит CSV с использованием Web Workers
|
|
2037
|
+
* @param {string|File} csvInput - CSV строка или File объект
|
|
2038
|
+
* @param {Object} [options] - Опции парсинга
|
|
2039
|
+
* @param {Function} [onProgress] - Callback прогресса
|
|
2040
|
+
* @returns {Promise<Array<Object>>} JSON данные
|
|
2182
2041
|
*/
|
|
2183
2042
|
async function parseCSVWithWorker(csvInput, options = {}, onProgress = null) {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
if (csvInput
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2043
|
+
// Создание pool если нужно
|
|
2044
|
+
const poolHolder = parseCSVWithWorker;
|
|
2045
|
+
if (!poolHolder.pool) {
|
|
2046
|
+
poolHolder.pool = createWorkerPool();
|
|
2047
|
+
}
|
|
2048
|
+
const pool = poolHolder.pool;
|
|
2049
|
+
// Подготовка CSV строки
|
|
2050
|
+
// ?????????? CSV ??????
|
|
2051
|
+
let csvPayload = csvInput;
|
|
2052
|
+
let transfer = null;
|
|
2053
|
+
if (csvInput instanceof File) {
|
|
2054
|
+
const buffer = await readFileAsArrayBuffer(csvInput);
|
|
2055
|
+
csvPayload = new Uint8Array(buffer);
|
|
2056
|
+
transfer = [buffer];
|
|
2057
|
+
}
|
|
2058
|
+
else if (csvInput instanceof ArrayBuffer) {
|
|
2059
|
+
csvPayload = csvInput;
|
|
2060
|
+
transfer = [csvInput];
|
|
2061
|
+
}
|
|
2062
|
+
else if (ArrayBuffer.isView(csvInput)) {
|
|
2063
|
+
csvPayload = csvInput;
|
|
2064
|
+
if (csvInput.buffer instanceof ArrayBuffer) {
|
|
2065
|
+
transfer = [csvInput.buffer];
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
else if (typeof csvInput !== 'string') {
|
|
2069
|
+
throw new ValidationError('Input must be a CSV string, File, or ArrayBuffer');
|
|
2070
|
+
}
|
|
2071
|
+
// ????????? ?????? ????? pool
|
|
2072
|
+
const execOptions = transfer ? { transfer } : {};
|
|
2073
|
+
return pool.exec('parseCSV', [csvPayload, options], execOptions, onProgress);
|
|
2215
2074
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
*
|
|
2219
|
-
* @private
|
|
2075
|
+
/**
|
|
2076
|
+
* Чтение файла как текст
|
|
2077
|
+
* @private
|
|
2220
2078
|
*/
|
|
2221
2079
|
async function readFileAsArrayBuffer(file) {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2080
|
+
return new Promise((resolve, reject) => {
|
|
2081
|
+
const reader = new FileReader();
|
|
2082
|
+
reader.onload = (event) => resolve(event.target.result);
|
|
2083
|
+
reader.onerror = (error) => reject(error);
|
|
2084
|
+
reader.readAsArrayBuffer(file);
|
|
2085
|
+
});
|
|
2228
2086
|
}
|
|
2229
|
-
|
|
2230
2087
|
// Экспорт для Node.js совместимости
|
|
2231
2088
|
if (typeof module !== 'undefined' && module.exports) {
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2089
|
+
module.exports = {
|
|
2090
|
+
WorkerPool,
|
|
2091
|
+
createWorkerPool,
|
|
2092
|
+
parseCSVWithWorker
|
|
2093
|
+
};
|
|
2237
2094
|
}
|
|
2238
2095
|
|
|
2239
2096
|
var workerPool = /*#__PURE__*/Object.freeze({
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2097
|
+
__proto__: null,
|
|
2098
|
+
WorkerPool: WorkerPool,
|
|
2099
|
+
createWorkerPool: createWorkerPool,
|
|
2100
|
+
parseCSVWithWorker: parseCSVWithWorker
|
|
2244
2101
|
});
|
|
2245
2102
|
|
|
2246
2103
|
// Браузерный entry point для jtcsv
|
|
2247
2104
|
// Экспортирует все функции с поддержкой браузера
|
|
2248
|
-
|
|
2105
|
+
const { jsonToCsv, preprocessData, deepUnwrap } = jsonToCsvBrowser;
|
|
2106
|
+
const { csvToJson, csvToJsonIterator, autoDetectDelimiter } = csvToJsonBrowser;
|
|
2107
|
+
/**
|
|
2108
|
+
* Ленивая инициализация Worker Pool
|
|
2109
|
+
*/
|
|
2249
2110
|
async function createWorkerPoolLazy(options = {}) {
|
|
2250
|
-
|
|
2251
|
-
|
|
2111
|
+
const mod = await Promise.resolve().then(function () { return workerPool; });
|
|
2112
|
+
return mod.createWorkerPool(options);
|
|
2252
2113
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2114
|
+
/**
|
|
2115
|
+
* Ленивый парсинг CSV с использованием Worker
|
|
2116
|
+
*/
|
|
2117
|
+
async function parseCSVWithWorkerLazy(csvInput, options = {}, onProgress) {
|
|
2118
|
+
const mod = await Promise.resolve().then(function () { return workerPool; });
|
|
2119
|
+
return mod.parseCSVWithWorker(csvInput, options, onProgress);
|
|
2120
|
+
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Асинхронная версия jsonToCsv
|
|
2123
|
+
*/
|
|
2124
|
+
async function jsonToCsvAsync(data, options = {}) {
|
|
2125
|
+
return jsonToCsv(data, options);
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Асинхронная версия csvToJson
|
|
2129
|
+
*/
|
|
2130
|
+
async function csvToJsonAsync(csv, options = {}) {
|
|
2131
|
+
return csvToJson(csv, options);
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Асинхронная версия parseCsvFile
|
|
2135
|
+
*/
|
|
2136
|
+
async function parseCsvFileAsync(file, options = {}) {
|
|
2137
|
+
return parseCsvFile(file, options);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Асинхронная версия autoDetectDelimiter
|
|
2141
|
+
*/
|
|
2142
|
+
async function autoDetectDelimiterAsync(csv) {
|
|
2143
|
+
return autoDetectDelimiter(csv);
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Асинхронная версия downloadAsCsv
|
|
2147
|
+
*/
|
|
2148
|
+
async function downloadAsCsvAsync(data, filename = 'export.csv', options = {}) {
|
|
2149
|
+
return downloadAsCsv(data, filename, options);
|
|
2256
2150
|
}
|
|
2257
|
-
|
|
2258
2151
|
// Основной экспорт
|
|
2259
2152
|
const jtcsv = {
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2153
|
+
// JSON to CSV функции
|
|
2154
|
+
jsonToCsv,
|
|
2155
|
+
preprocessData,
|
|
2156
|
+
downloadAsCsv,
|
|
2157
|
+
deepUnwrap,
|
|
2158
|
+
// CSV to JSON функции
|
|
2159
|
+
csvToJson,
|
|
2160
|
+
csvToJsonIterator,
|
|
2161
|
+
parseCsvFile,
|
|
2162
|
+
parseCsvFileStream,
|
|
2163
|
+
jsonToCsvStream,
|
|
2164
|
+
jsonToNdjsonStream,
|
|
2165
|
+
csvToJsonStream,
|
|
2166
|
+
autoDetectDelimiter,
|
|
2167
|
+
// Web Workers функции
|
|
2168
|
+
createWorkerPool,
|
|
2169
|
+
parseCSVWithWorker,
|
|
2170
|
+
createWorkerPoolLazy,
|
|
2171
|
+
parseCSVWithWorkerLazy,
|
|
2172
|
+
// Асинхронные функции
|
|
2173
|
+
jsonToCsvAsync,
|
|
2174
|
+
csvToJsonAsync,
|
|
2175
|
+
parseCsvFileAsync,
|
|
2176
|
+
autoDetectDelimiterAsync,
|
|
2177
|
+
downloadAsCsvAsync,
|
|
2178
|
+
// Error classes
|
|
2179
|
+
ValidationError,
|
|
2180
|
+
SecurityError,
|
|
2181
|
+
FileSystemError,
|
|
2182
|
+
ParsingError,
|
|
2183
|
+
LimitError,
|
|
2184
|
+
ConfigurationError,
|
|
2185
|
+
ERROR_CODES,
|
|
2186
|
+
// Удобные алиасы
|
|
2187
|
+
parse: csvToJson,
|
|
2188
|
+
unparse: jsonToCsv,
|
|
2189
|
+
parseAsync: csvToJsonAsync,
|
|
2190
|
+
unparseAsync: jsonToCsvAsync,
|
|
2191
|
+
// Версия
|
|
2192
|
+
version: '2.0.0-browser'
|
|
2292
2193
|
};
|
|
2293
|
-
|
|
2294
2194
|
// Экспорт для разных сред
|
|
2295
2195
|
if (typeof module !== 'undefined' && module.exports) {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2196
|
+
// Node.js CommonJS
|
|
2197
|
+
module.exports = jtcsv;
|
|
2198
|
+
}
|
|
2199
|
+
else if (typeof define === 'function' && define.amd) {
|
|
2200
|
+
// AMD
|
|
2201
|
+
define([], () => jtcsv);
|
|
2202
|
+
}
|
|
2203
|
+
else if (typeof window !== 'undefined') {
|
|
2204
|
+
// Браузер (глобальная переменная)
|
|
2205
|
+
window.jtcsv = jtcsv;
|
|
2304
2206
|
}
|
|
2305
2207
|
|
|
2306
|
-
export { ConfigurationError, ERROR_CODES, FileSystemError, LimitError, ParsingError, SecurityError, ValidationError, autoDetectDelimiter, createWorkerPool, createWorkerPoolLazy, csvToJson, csvToJsonIterator, csvToJsonStream, deepUnwrap, jtcsv as default, downloadAsCsv, jsonToCsv, jsonToCsvStream, jsonToNdjsonStream, parseCSVWithWorker, parseCSVWithWorkerLazy, parseCsvFile, parseCsvFileStream, preprocessData };
|
|
2208
|
+
export { ConfigurationError, ERROR_CODES, FileSystemError, LimitError, ParsingError, SecurityError, ValidationError, autoDetectDelimiter, autoDetectDelimiterAsync, createWorkerPool, createWorkerPoolLazy, csvToJson, csvToJsonAsync, csvToJsonIterator, csvToJsonStream, deepUnwrap, jtcsv as default, downloadAsCsv, downloadAsCsvAsync, jsonToCsv, jsonToCsvAsync, jsonToCsvStream, jsonToNdjsonStream, parseCSVWithWorker, parseCSVWithWorkerLazy, parseCsvFile, parseCsvFileAsync, parseCsvFileStream, preprocessData };
|
|
2307
2209
|
//# sourceMappingURL=jtcsv.esm.js.map
|