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