jtcsv 3.0.0 → 3.1.0

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