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
@@ -0,0 +1,494 @@
1
+ // Браузерная версия CSV to JSON конвертера
2
+ // Адаптирована для работы в браузере без Node.js API
3
+
4
+ import {
5
+ ValidationError,
6
+ ParsingError,
7
+ LimitError,
8
+ ConfigurationError,
9
+ safeExecute
10
+ } from './errors-browser';
11
+
12
+ import type { CsvToJsonOptions } from '../types';
13
+
14
+ /**
15
+ * Валидация опций парсинга
16
+ * @private
17
+ */
18
+ function validateCsvOptions(options: CsvToJsonOptions): boolean {
19
+ // Validate options
20
+ if (options && typeof options !== 'object') {
21
+ throw new ConfigurationError('Options must be an object');
22
+ }
23
+
24
+ // Validate delimiter
25
+ if (options?.delimiter && typeof options.delimiter !== 'string') {
26
+ throw new ConfigurationError('Delimiter must be a string');
27
+ }
28
+
29
+ if (options?.delimiter && options.delimiter.length !== 1) {
30
+ throw new ConfigurationError('Delimiter must be a single character');
31
+ }
32
+
33
+ // Validate autoDetect
34
+ if (options?.autoDetect !== undefined && typeof options.autoDetect !== 'boolean') {
35
+ throw new ConfigurationError('autoDetect must be a boolean');
36
+ }
37
+
38
+ // Validate candidates
39
+ if (options?.candidates && !Array.isArray(options.candidates)) {
40
+ throw new ConfigurationError('candidates must be an array');
41
+ }
42
+
43
+ // Validate maxRows
44
+ if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
45
+ throw new ConfigurationError('maxRows must be a positive number');
46
+ }
47
+
48
+ if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
49
+ throw new ConfigurationError('warnExtraFields must be a boolean');
50
+ }
51
+
52
+ if (options?.repairRowShifts !== undefined && typeof options.repairRowShifts !== 'boolean') {
53
+ throw new ConfigurationError('repairRowShifts must be a boolean');
54
+ }
55
+
56
+ if (options?.normalizeQuotes !== undefined && typeof options.normalizeQuotes !== 'boolean') {
57
+ throw new ConfigurationError('normalizeQuotes must be a boolean');
58
+ }
59
+
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Автоматическое определение разделителя
65
+ * @private
66
+ */
67
+ function autoDetectDelimiter(text: string, candidates: string[] = [',', ';', '\t', '|']): string {
68
+ if (!text || typeof text !== 'string') {
69
+ return ',';
70
+ }
71
+
72
+ const firstLine = text.split('\n')[0];
73
+ if (!firstLine) {
74
+ return ',';
75
+ }
76
+
77
+ let bestCandidate = ',';
78
+ let bestCount = 0;
79
+
80
+ for (const candidate of candidates) {
81
+ const count = (firstLine.match(new RegExp(candidate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
82
+ if (count > bestCount) {
83
+ bestCount = count;
84
+ bestCandidate = candidate;
85
+ }
86
+ }
87
+
88
+ return bestCandidate;
89
+ }
90
+
91
+ function isEmptyValue(value: any): boolean {
92
+ return value === undefined || value === null || value === '';
93
+ }
94
+
95
+ function hasOddQuotes(value: any): boolean {
96
+ if (typeof value !== 'string') {
97
+ return false;
98
+ }
99
+ let count = 0;
100
+ for (let i = 0; i < value.length; i++) {
101
+ if (value[i] === '"') {
102
+ count++;
103
+ }
104
+ }
105
+ return count % 2 === 1;
106
+ }
107
+
108
+ function hasAnyQuotes(value: any): boolean {
109
+ return typeof value === 'string' && value.includes('"');
110
+ }
111
+
112
+ function normalizeQuotesInField(value: any): any {
113
+ if (typeof value !== 'string') {
114
+ return value;
115
+ }
116
+ // Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
117
+ // Проверяем, выглядит ли значение как JSON (объект или массив)
118
+ if ((value.startsWith('{') && value.endsWith('}')) ||
119
+ (value.startsWith('[') && value.endsWith(']'))) {
120
+ return value; // Возвращаем как есть для JSON
121
+ }
122
+
123
+ let normalized = value.replace(/"{2,}/g, '"');
124
+ // Убираем правило, которое ломает JSON: не заменяем "," на ","
125
+ // normalized = normalized.replace(/"\s*,\s*"/g, ',');
126
+ normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
127
+ if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
128
+ normalized = normalized.slice(1, -1);
129
+ }
130
+ return normalized;
131
+ }
132
+
133
+ function normalizePhoneValue(value: any): any {
134
+ if (typeof value !== 'string') {
135
+ return value;
136
+ }
137
+ const trimmed = value.trim();
138
+ if (trimmed === '') {
139
+ return trimmed;
140
+ }
141
+ return trimmed.replace(/["'\\]/g, '');
142
+ }
143
+
144
+ function normalizeRowQuotes(row: Record<string, any>, headers: string[]): Record<string, any> {
145
+ const normalized: Record<string, any> = {};
146
+ const phoneKeys = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
147
+ for (const header of headers) {
148
+ const baseValue = normalizeQuotesInField(row[header]);
149
+ if (phoneKeys.has(String(header).toLowerCase())) {
150
+ normalized[header] = normalizePhoneValue(baseValue);
151
+ } else {
152
+ normalized[header] = baseValue;
153
+ }
154
+ }
155
+ return normalized;
156
+ }
157
+
158
+ function looksLikeUserAgent(value: any): boolean {
159
+ if (typeof value !== 'string') {
160
+ return false;
161
+ }
162
+ return /Mozilla\/|Opera\/|MSIE|AppleWebKit|Gecko|Safari|Chrome\//.test(value);
163
+ }
164
+
165
+ function isHexColor(value: any): boolean {
166
+ return typeof value === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
167
+ }
168
+
169
+ function repairShiftedRows(
170
+ rows: Record<string, any>[],
171
+ headers: string[],
172
+ options: { normalizeQuotes?: boolean } = {}
173
+ ): Record<string, any>[] {
174
+ if (!Array.isArray(rows) || rows.length === 0 || headers.length === 0) {
175
+ return rows;
176
+ }
177
+
178
+ const headerCount = headers.length;
179
+ const merged: Record<string, any>[] = [];
180
+ let index = 0;
181
+
182
+ while (index < rows.length) {
183
+ const row = rows[index];
184
+ if (!row || typeof row !== 'object') {
185
+ merged.push(row);
186
+ index++;
187
+ continue;
188
+ }
189
+
190
+ const values = headers.map((header) => row[header]);
191
+ let lastNonEmpty = -1;
192
+ for (let i = headerCount - 1; i >= 0; i--) {
193
+ if (!isEmptyValue(values[i])) {
194
+ lastNonEmpty = i;
195
+ break;
196
+ }
197
+ }
198
+
199
+ const missingCount = headerCount - 1 - lastNonEmpty;
200
+ if (lastNonEmpty >= 0 && missingCount > 0 && index + 1 < rows.length) {
201
+ const nextRow = rows[index + 1];
202
+ if (nextRow && typeof nextRow === 'object') {
203
+ const nextValues = headers.map((header) => nextRow[header]);
204
+ const nextTrailingEmpty = nextValues
205
+ .slice(headerCount - missingCount)
206
+ .every((value) => isEmptyValue(value));
207
+
208
+ const leadValues = nextValues
209
+ .slice(0, missingCount)
210
+ .filter((value) => !isEmptyValue(value));
211
+ const shouldMerge = nextTrailingEmpty
212
+ && leadValues.length > 0
213
+ && (hasOddQuotes(values[lastNonEmpty]) || hasAnyQuotes(values[lastNonEmpty]));
214
+
215
+ if (shouldMerge) {
216
+ const toAppend = leadValues.map((value) => String(value));
217
+
218
+ if (toAppend.length > 0) {
219
+ const base = isEmptyValue(values[lastNonEmpty]) ? '' : String(values[lastNonEmpty]);
220
+ values[lastNonEmpty] = base ? `${base}\n${toAppend.join('\n')}` : toAppend.join('\n');
221
+ }
222
+
223
+ for (let i = 0; i < missingCount; i++) {
224
+ values[lastNonEmpty + 1 + i] = nextValues[missingCount + i];
225
+ }
226
+
227
+ const mergedRow: Record<string, any> = {};
228
+ for (let i = 0; i < headerCount; i++) {
229
+ mergedRow[headers[i]] = values[i];
230
+ }
231
+
232
+ merged.push(mergedRow);
233
+ index += 2;
234
+ continue;
235
+ }
236
+ }
237
+ }
238
+
239
+ if (index + 1 < rows.length && headerCount >= 6) {
240
+ const nextRow = rows[index + 1];
241
+ if (nextRow && typeof nextRow === 'object') {
242
+ const nextHex = nextRow[headers[4]];
243
+ const nextUserAgentHead = nextRow[headers[2]];
244
+ const nextUserAgentTail = nextRow[headers[3]];
245
+ const shouldMergeUserAgent = isEmptyValue(values[4])
246
+ && isEmptyValue(values[5])
247
+ && isHexColor(nextHex)
248
+ && (looksLikeUserAgent(nextUserAgentHead) || looksLikeUserAgent(nextUserAgentTail));
249
+
250
+ if (shouldMergeUserAgent) {
251
+ const addressParts = [values[3], nextRow[headers[0]], nextRow[headers[1]]]
252
+ .filter((value) => !isEmptyValue(value))
253
+ .map((value) => String(value));
254
+ values[3] = addressParts.join('\n');
255
+
256
+ const uaHead = isEmptyValue(nextUserAgentHead) ? '' : String(nextUserAgentHead);
257
+ const uaTail = isEmptyValue(nextUserAgentTail) ? '' : String(nextUserAgentTail);
258
+ const joiner = uaHead && uaTail ? (uaTail.startsWith(' ') ? '' : ',') : '';
259
+ values[4] = uaHead + joiner + uaTail;
260
+ values[5] = String(nextHex);
261
+
262
+ const mergedRow: Record<string, any> = {};
263
+ for (let i = 0; i < headerCount; i++) {
264
+ mergedRow[headers[i]] = values[i];
265
+ }
266
+
267
+ merged.push(mergedRow);
268
+ index += 2;
269
+ continue;
270
+ }
271
+ }
272
+ }
273
+
274
+ merged.push(row);
275
+ index++;
276
+ }
277
+
278
+ if (options.normalizeQuotes) {
279
+ return merged.map((row) => normalizeRowQuotes(row, headers));
280
+ }
281
+
282
+ return merged;
283
+ }
284
+
285
+ /**
286
+ * Парсинг CSV строки в массив объектов
287
+ *
288
+ * @param csvText - CSV текст для парсинга
289
+ * @param options - Опции парсинга
290
+ * @returns Массив объектов
291
+ */
292
+ export function csvToJson(csvText: string, options: CsvToJsonOptions = {}): any[] {
293
+ return safeExecute(() => {
294
+ validateCsvOptions(options);
295
+
296
+ if (typeof csvText !== 'string') {
297
+ throw new ValidationError('CSV text must be a string');
298
+ }
299
+
300
+ if (csvText.trim() === '') {
301
+ return [];
302
+ }
303
+
304
+ // Определение разделителя
305
+ const delimiter = options.delimiter ||
306
+ (options.autoDetect !== false ? autoDetectDelimiter(csvText, options.candidates) : ',');
307
+
308
+ // Разделение на строки
309
+ const lines = csvText.split('\n').filter(line => line.trim() !== '');
310
+ if (lines.length === 0) {
311
+ return [];
312
+ }
313
+
314
+ // Парсинг заголовков
315
+ const headers = lines[0].split(delimiter).map(h => h.trim());
316
+ const {
317
+ repairRowShifts = true,
318
+ normalizeQuotes = true
319
+ } = options || {};
320
+
321
+ // Ограничение количества строк
322
+ const maxRows = options.maxRows || Infinity;
323
+ const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
324
+
325
+ // Парсинг данных
326
+ const result = [];
327
+
328
+ for (let i = 0; i < dataRows.length; i++) {
329
+ const line = dataRows[i];
330
+ const values = line.split(delimiter);
331
+ const row: Record<string, any> = {};
332
+
333
+ for (let j = 0; j < headers.length; j++) {
334
+ const header = headers[j];
335
+ const value = j < values.length ? values[j].trim() : '';
336
+
337
+ // Попытка парсинга чисел
338
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
339
+ row[header] = parseFloat(value);
340
+ } else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
341
+ row[header] = value.toLowerCase() === 'true';
342
+ } else {
343
+ row[header] = value;
344
+ }
345
+ }
346
+
347
+ result.push(row);
348
+ }
349
+
350
+ if (repairRowShifts) {
351
+ return repairShiftedRows(result, headers, { normalizeQuotes });
352
+ }
353
+
354
+ if (normalizeQuotes) {
355
+ return result.map((row) => normalizeRowQuotes(row, headers));
356
+ }
357
+
358
+ return result;
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Асинхронная версия csvToJson
364
+ */
365
+ export async function csvToJsonAsync(csvText: string, options: CsvToJsonOptions = {}): Promise<any[]> {
366
+ return csvToJson(csvText, options);
367
+ }
368
+
369
+ /**
370
+ * Создает итератор для потокового парсинга CSV
371
+ *
372
+ * @param input - CSV текст, File или Blob
373
+ * @param options - Опции парсинга
374
+ * @returns AsyncGenerator
375
+ */
376
+ export async function* csvToJsonIterator(input: string | File | Blob, options: CsvToJsonOptions = {}): AsyncGenerator<any> {
377
+ validateCsvOptions(options);
378
+
379
+ let csvText: string;
380
+
381
+ if (typeof input === 'string') {
382
+ csvText = input;
383
+ } else if (input instanceof File || input instanceof Blob) {
384
+ csvText = await input.text();
385
+ } else {
386
+ throw new ValidationError('Input must be string, File or Blob');
387
+ }
388
+
389
+ if (csvText.trim() === '') {
390
+ return;
391
+ }
392
+
393
+ // Определение разделителя
394
+ const delimiter = options.delimiter ||
395
+ (options.autoDetect !== false ? autoDetectDelimiter(csvText, options.candidates) : ',');
396
+
397
+ // Разделение на строки
398
+ const lines = csvText.split('\n').filter(line => line.trim() !== '');
399
+ if (lines.length === 0) {
400
+ return;
401
+ }
402
+
403
+ // Парсинг заголовков
404
+ const headers = lines[0].split(delimiter).map(h => h.trim());
405
+ const {
406
+ repairRowShifts = true,
407
+ normalizeQuotes = true
408
+ } = options || {};
409
+
410
+ // Ограничение количества строк
411
+ const maxRows = options.maxRows || Infinity;
412
+ const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
413
+
414
+ // Возврат данных по одной строке
415
+ const parsedRows = [];
416
+ for (let i = 0; i < dataRows.length; i++) {
417
+ const line = dataRows[i];
418
+ const values = line.split(delimiter);
419
+ const row: Record<string, any> = {};
420
+
421
+ for (let j = 0; j < headers.length; j++) {
422
+ const header = headers[j];
423
+ const value = j < values.length ? values[j].trim() : '';
424
+
425
+ // Try parsing numbers
426
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
427
+ row[header] = parseFloat(value);
428
+ } else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
429
+ row[header] = value.toLowerCase() === 'true';
430
+ } else {
431
+ row[header] = value;
432
+ }
433
+ }
434
+
435
+ parsedRows.push(row);
436
+ }
437
+
438
+ const finalRows = repairRowShifts
439
+ ? repairShiftedRows(parsedRows, headers, { normalizeQuotes })
440
+ : (normalizeQuotes
441
+ ? parsedRows.map((row) => normalizeRowQuotes(row, headers))
442
+ : parsedRows);
443
+
444
+ for (const row of finalRows) {
445
+ yield row;
446
+ }
447
+
448
+ }
449
+
450
+ /**
451
+ * Асинхронная версия csvToJsonIterator (псевдоним)
452
+ */
453
+ export const csvToJsonIteratorAsync = csvToJsonIterator;
454
+
455
+ /**
456
+ * Парсинг CSV с обработкой ошибок
457
+ *
458
+ * @param csvText - CSV текст
459
+ * @param options - Опции парсинга
460
+ * @returns Результат парсинга или null при ошибке
461
+ */
462
+ export function parseCsvSafe(csvText: string, options: CsvToJsonOptions = {}): any[] | null {
463
+ try {
464
+ return csvToJson(csvText, options);
465
+ } catch (error) {
466
+ console.error('CSV parsing error:', error);
467
+ return null;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Асинхронная версия parseCsvSafe
473
+ */
474
+ export async function parseCsvSafeAsync(csvText: string, options: CsvToJsonOptions = {}): Promise<any[] | null> {
475
+ try {
476
+ return await csvToJsonAsync(csvText, options);
477
+ } catch (error) {
478
+ console.error('CSV parsing error:', error);
479
+ return null;
480
+ }
481
+ }
482
+
483
+ // Экспорт для Node.js совместимости
484
+ if (typeof module !== 'undefined' && module.exports) {
485
+ module.exports = {
486
+ csvToJson,
487
+ csvToJsonAsync,
488
+ csvToJsonIterator,
489
+ csvToJsonIteratorAsync,
490
+ parseCsvSafe,
491
+ parseCsvSafeAsync,
492
+ autoDetectDelimiter
493
+ };
494
+ }