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