jtcsv 2.2.8 → 3.1.0

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