jtcsv 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +205 -146
  2. package/bin/jtcsv.ts +280 -202
  3. package/browser.d.ts +142 -0
  4. package/dist/benchmark.js +446 -0
  5. package/dist/benchmark.js.map +1 -0
  6. package/dist/bin/jtcsv.js +1940 -0
  7. package/dist/bin/jtcsv.js.map +1 -0
  8. package/dist/csv-to-json.js +1261 -0
  9. package/dist/csv-to-json.js.map +1 -0
  10. package/dist/errors.js +291 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/eslint.config.js +147 -0
  13. package/dist/eslint.config.js.map +1 -0
  14. package/dist/index-core.js +95 -0
  15. package/dist/index-core.js.map +1 -0
  16. package/dist/index.js +93 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/json-save.js +229 -0
  19. package/dist/json-save.js.map +1 -0
  20. package/dist/json-to-csv.js +576 -0
  21. package/dist/json-to-csv.js.map +1 -0
  22. package/dist/jtcsv-core.cjs.js +336 -7
  23. package/dist/jtcsv-core.cjs.js.map +1 -1
  24. package/dist/jtcsv-core.esm.js +336 -7
  25. package/dist/jtcsv-core.esm.js.map +1 -1
  26. package/dist/jtcsv-core.umd.js +336 -7
  27. package/dist/jtcsv-core.umd.js.map +1 -1
  28. package/dist/jtcsv-full.cjs.js +336 -7
  29. package/dist/jtcsv-full.cjs.js.map +1 -1
  30. package/dist/jtcsv-full.esm.js +336 -7
  31. package/dist/jtcsv-full.esm.js.map +1 -1
  32. package/dist/jtcsv-full.umd.js +336 -7
  33. package/dist/jtcsv-full.umd.js.map +1 -1
  34. package/dist/jtcsv-workers.esm.js +9 -0
  35. package/dist/jtcsv-workers.esm.js.map +1 -1
  36. package/dist/jtcsv-workers.umd.js +9 -0
  37. package/dist/jtcsv-workers.umd.js.map +1 -1
  38. package/dist/jtcsv.cjs.js +1998 -2092
  39. package/dist/jtcsv.cjs.js.map +1 -1
  40. package/dist/jtcsv.esm.js +1994 -2092
  41. package/dist/jtcsv.esm.js.map +1 -1
  42. package/dist/jtcsv.umd.js +2157 -2251
  43. package/dist/jtcsv.umd.js.map +1 -1
  44. package/dist/plugins/express-middleware/index.js +350 -0
  45. package/dist/plugins/express-middleware/index.js.map +1 -0
  46. package/dist/plugins/fastify-plugin/index.js +315 -0
  47. package/dist/plugins/fastify-plugin/index.js.map +1 -0
  48. package/dist/plugins/hono/index.js +111 -0
  49. package/dist/plugins/hono/index.js.map +1 -0
  50. package/dist/plugins/nestjs/index.js +192 -0
  51. package/dist/plugins/nestjs/index.js.map +1 -0
  52. package/dist/plugins/nuxt/index.js +53 -0
  53. package/dist/plugins/nuxt/index.js.map +1 -0
  54. package/dist/plugins/remix/index.js +133 -0
  55. package/dist/plugins/remix/index.js.map +1 -0
  56. package/dist/plugins/sveltekit/index.js +155 -0
  57. package/dist/plugins/sveltekit/index.js.map +1 -0
  58. package/dist/plugins/trpc/index.js +136 -0
  59. package/dist/plugins/trpc/index.js.map +1 -0
  60. package/dist/run-demo.js +49 -0
  61. package/dist/run-demo.js.map +1 -0
  62. package/dist/src/browser/browser-functions.js +193 -0
  63. package/dist/src/browser/browser-functions.js.map +1 -0
  64. package/dist/src/browser/core.js +123 -0
  65. package/dist/src/browser/core.js.map +1 -0
  66. package/dist/src/browser/csv-to-json-browser.js +353 -0
  67. package/dist/src/browser/csv-to-json-browser.js.map +1 -0
  68. package/dist/src/browser/errors-browser.js +219 -0
  69. package/dist/src/browser/errors-browser.js.map +1 -0
  70. package/dist/src/browser/extensions/plugins.js +106 -0
  71. package/dist/src/browser/extensions/plugins.js.map +1 -0
  72. package/dist/src/browser/extensions/workers.js +66 -0
  73. package/dist/src/browser/extensions/workers.js.map +1 -0
  74. package/dist/src/browser/index.js +140 -0
  75. package/dist/src/browser/index.js.map +1 -0
  76. package/dist/src/browser/json-to-csv-browser.js +225 -0
  77. package/dist/src/browser/json-to-csv-browser.js.map +1 -0
  78. package/dist/src/browser/streams.js +340 -0
  79. package/dist/src/browser/streams.js.map +1 -0
  80. package/dist/src/browser/workers/csv-parser.worker.js +264 -0
  81. package/dist/src/browser/workers/csv-parser.worker.js.map +1 -0
  82. package/dist/src/browser/workers/worker-pool.js +338 -0
  83. package/dist/src/browser/workers/worker-pool.js.map +1 -0
  84. package/dist/src/core/delimiter-cache.js +196 -0
  85. package/dist/src/core/delimiter-cache.js.map +1 -0
  86. package/dist/src/core/node-optimizations.js +279 -0
  87. package/dist/src/core/node-optimizations.js.map +1 -0
  88. package/dist/src/core/plugin-system.js +399 -0
  89. package/dist/src/core/plugin-system.js.map +1 -0
  90. package/dist/src/core/transform-hooks.js +348 -0
  91. package/dist/src/core/transform-hooks.js.map +1 -0
  92. package/dist/src/engines/fast-path-engine-new.js +262 -0
  93. package/dist/src/engines/fast-path-engine-new.js.map +1 -0
  94. package/dist/src/engines/fast-path-engine.js +671 -0
  95. package/dist/src/engines/fast-path-engine.js.map +1 -0
  96. package/dist/src/errors.js +18 -0
  97. package/dist/src/errors.js.map +1 -0
  98. package/dist/src/formats/ndjson-parser.js +332 -0
  99. package/dist/src/formats/ndjson-parser.js.map +1 -0
  100. package/dist/src/formats/tsv-parser.js +230 -0
  101. package/dist/src/formats/tsv-parser.js.map +1 -0
  102. package/dist/src/index-with-plugins.js +259 -0
  103. package/dist/src/index-with-plugins.js.map +1 -0
  104. package/dist/src/types/index.js +3 -0
  105. package/dist/src/types/index.js.map +1 -0
  106. package/dist/src/utils/bom-utils.js +267 -0
  107. package/dist/src/utils/bom-utils.js.map +1 -0
  108. package/dist/src/utils/encoding-support.js +77 -0
  109. package/dist/src/utils/encoding-support.js.map +1 -0
  110. package/dist/src/utils/schema-validator.js +609 -0
  111. package/dist/src/utils/schema-validator.js.map +1 -0
  112. package/dist/src/utils/transform-loader.js +281 -0
  113. package/dist/src/utils/transform-loader.js.map +1 -0
  114. package/dist/src/utils/validators.js +40 -0
  115. package/dist/src/utils/validators.js.map +1 -0
  116. package/dist/src/utils/zod-adapter.js +144 -0
  117. package/dist/src/utils/zod-adapter.js.map +1 -0
  118. package/{src → dist/src}/web-server/index.js +251 -286
  119. package/dist/src/web-server/index.js.map +1 -0
  120. package/dist/src/workers/csv-multithreaded.js +211 -0
  121. package/dist/src/workers/csv-multithreaded.js.map +1 -0
  122. package/dist/src/workers/csv-parser.worker.js +179 -0
  123. package/dist/src/workers/csv-parser.worker.js.map +1 -0
  124. package/dist/src/workers/worker-pool.js +228 -0
  125. package/dist/src/workers/worker-pool.js.map +1 -0
  126. package/dist/stream-csv-to-json.js +664 -0
  127. package/dist/stream-csv-to-json.js.map +1 -0
  128. package/dist/stream-json-to-csv.js +389 -0
  129. package/dist/stream-json-to-csv.js.map +1 -0
  130. package/examples/advanced/conditional-transformations.ts +2 -2
  131. package/examples/advanced/performance-optimization.ts +2 -2
  132. package/examples/cli-advanced-usage.md +2 -0
  133. package/examples/cli-tool.ts +1 -1
  134. package/examples/large-dataset-example.ts +2 -2
  135. package/examples/simple-usage.ts +2 -2
  136. package/examples/streaming-example.ts +1 -1
  137. package/index.d.ts +186 -15
  138. package/package.json +243 -305
  139. package/plugins.d.ts +37 -0
  140. package/schema.d.ts +103 -0
  141. package/src/browser/csv-to-json-browser.ts +233 -3
  142. package/src/browser/errors-browser.ts +45 -28
  143. package/src/browser/json-to-csv-browser.ts +81 -5
  144. package/src/browser/streams.ts +73 -6
  145. package/src/core/delimiter-cache.ts +21 -11
  146. package/src/core/plugin-system.ts +343 -155
  147. package/src/core/transform-hooks.ts +20 -12
  148. package/src/engines/fast-path-engine.ts +48 -32
  149. package/src/errors.ts +1 -72
  150. package/src/formats/ndjson-parser.ts +6 -0
  151. package/src/formats/tsv-parser.ts +6 -0
  152. package/src/types/index.ts +21 -1
  153. package/src/utils/validators.ts +35 -0
  154. package/src/web-server/index.ts +1 -1
  155. package/bin/jtcsv.js +0 -2532
  156. package/csv-to-json.js +0 -711
  157. package/errors.js +0 -394
  158. package/examples/advanced/conditional-transformations.js +0 -446
  159. package/examples/advanced/csv-parser.worker.js +0 -89
  160. package/examples/advanced/nested-objects-example.js +0 -306
  161. package/examples/advanced/performance-optimization.js +0 -504
  162. package/examples/advanced/run-demo-server.js +0 -116
  163. package/examples/cli-batch-processing.js +0 -38
  164. package/examples/cli-tool.js +0 -183
  165. package/examples/error-handling.js +0 -338
  166. package/examples/express-api.js +0 -164
  167. package/examples/large-dataset-example.js +0 -182
  168. package/examples/ndjson-processing.js +0 -434
  169. package/examples/plugin-excel-exporter.js +0 -406
  170. package/examples/schema-validation.js +0 -640
  171. package/examples/simple-usage.js +0 -282
  172. package/examples/streaming-example.js +0 -418
  173. package/examples/web-workers-advanced.js +0 -28
  174. package/index.js +0 -82
  175. package/json-save.js +0 -255
  176. package/json-to-csv.js +0 -668
  177. package/plugins/README.md +0 -91
  178. package/plugins/express-middleware/README.md +0 -83
  179. package/plugins/express-middleware/example.js +0 -135
  180. package/plugins/express-middleware/example.ts +0 -135
  181. package/plugins/express-middleware/index.d.ts +0 -114
  182. package/plugins/express-middleware/index.js +0 -512
  183. package/plugins/express-middleware/index.ts +0 -557
  184. package/plugins/express-middleware/package.json +0 -52
  185. package/plugins/fastify-plugin/index.js +0 -404
  186. package/plugins/fastify-plugin/index.ts +0 -443
  187. package/plugins/fastify-plugin/package.json +0 -55
  188. package/plugins/hono/README.md +0 -28
  189. package/plugins/hono/index.d.ts +0 -12
  190. package/plugins/hono/index.js +0 -36
  191. package/plugins/hono/index.ts +0 -226
  192. package/plugins/hono/package.json +0 -35
  193. package/plugins/nestjs/README.md +0 -35
  194. package/plugins/nestjs/index.d.ts +0 -25
  195. package/plugins/nestjs/index.js +0 -77
  196. package/plugins/nestjs/index.ts +0 -201
  197. package/plugins/nestjs/package.json +0 -37
  198. package/plugins/nextjs-api/README.md +0 -57
  199. package/plugins/nextjs-api/examples/ConverterComponent.jsx +0 -386
  200. package/plugins/nextjs-api/examples/ConverterComponent.tsx +0 -386
  201. package/plugins/nextjs-api/examples/api-convert.js +0 -67
  202. package/plugins/nextjs-api/examples/api-convert.ts +0 -67
  203. package/plugins/nextjs-api/index.js +0 -387
  204. package/plugins/nextjs-api/index.tsx +0 -339
  205. package/plugins/nextjs-api/package.json +0 -63
  206. package/plugins/nextjs-api/route.js +0 -370
  207. package/plugins/nextjs-api/route.ts +0 -370
  208. package/plugins/nuxt/README.md +0 -24
  209. package/plugins/nuxt/index.js +0 -21
  210. package/plugins/nuxt/index.ts +0 -94
  211. package/plugins/nuxt/package.json +0 -35
  212. package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
  213. package/plugins/nuxt/runtime/composables/useJtcsv.ts +0 -100
  214. package/plugins/nuxt/runtime/plugin.js +0 -6
  215. package/plugins/nuxt/runtime/plugin.ts +0 -71
  216. package/plugins/remix/README.md +0 -26
  217. package/plugins/remix/index.d.ts +0 -16
  218. package/plugins/remix/index.js +0 -62
  219. package/plugins/remix/index.ts +0 -260
  220. package/plugins/remix/package.json +0 -35
  221. package/plugins/sveltekit/README.md +0 -28
  222. package/plugins/sveltekit/index.d.ts +0 -17
  223. package/plugins/sveltekit/index.js +0 -54
  224. package/plugins/sveltekit/index.ts +0 -301
  225. package/plugins/sveltekit/package.json +0 -33
  226. package/plugins/trpc/README.md +0 -25
  227. package/plugins/trpc/index.d.ts +0 -7
  228. package/plugins/trpc/index.js +0 -32
  229. package/plugins/trpc/index.ts +0 -267
  230. package/plugins/trpc/package.json +0 -34
  231. package/src/browser/browser-functions.js +0 -219
  232. package/src/browser/core.js +0 -92
  233. package/src/browser/csv-to-json-browser.js +0 -722
  234. package/src/browser/errors-browser.js +0 -212
  235. package/src/browser/extensions/plugins.js +0 -92
  236. package/src/browser/extensions/workers.js +0 -39
  237. package/src/browser/index.js +0 -113
  238. package/src/browser/json-to-csv-browser.js +0 -319
  239. package/src/browser/streams.js +0 -403
  240. package/src/browser/workers/csv-parser.worker.js +0 -377
  241. package/src/browser/workers/worker-pool.js +0 -527
  242. package/src/core/delimiter-cache.js +0 -200
  243. package/src/core/node-optimizations.js +0 -408
  244. package/src/core/plugin-system.js +0 -494
  245. package/src/core/transform-hooks.js +0 -350
  246. package/src/engines/fast-path-engine-new.js +0 -338
  247. package/src/engines/fast-path-engine.js +0 -844
  248. package/src/errors.js +0 -26
  249. package/src/formats/ndjson-parser.js +0 -467
  250. package/src/formats/tsv-parser.js +0 -339
  251. package/src/index-with-plugins.js +0 -378
  252. package/src/utils/bom-utils.js +0 -259
  253. package/src/utils/encoding-support.js +0 -124
  254. package/src/utils/schema-validator.js +0 -594
  255. package/src/utils/transform-loader.js +0 -205
  256. package/src/utils/zod-adapter.js +0 -170
  257. package/stream-csv-to-json.js +0 -560
  258. package/stream-json-to-csv.js +0 -465
@@ -15,6 +15,9 @@
15
15
  this.name = 'JTCSVError';
16
16
  this.code = code;
17
17
  this.details = details;
18
+ this.hint = details.hint;
19
+ this.docs = details.docs;
20
+ this.context = details.context;
18
21
  // Сохранение stack trace
19
22
  if (Error.captureStackTrace) {
20
23
  Error.captureStackTrace(this, JTCSVError);
@@ -210,6 +213,12 @@
210
213
  if (error instanceof LimitError && error.limit && error.actual) {
211
214
  message += ` (limit: ${error.limit}, actual: ${error.actual})`;
212
215
  }
216
+ if (error.hint) {
217
+ message += `\nHint: ${error.hint}`;
218
+ }
219
+ if (error.docs) {
220
+ message += `\nDocs: ${error.docs}`;
221
+ }
213
222
  }
214
223
  if (includeStack && error.stack) {
215
224
  message += `\n${error.stack}`;
@@ -291,6 +300,9 @@
291
300
  if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
292
301
  throw new ConfigurationError('rfc4180Compliant must be a boolean');
293
302
  }
303
+ if (options?.normalizeQuotes !== undefined && typeof options.normalizeQuotes !== 'boolean') {
304
+ throw new ConfigurationError('normalizeQuotes must be a boolean');
305
+ }
294
306
  return true;
295
307
  }
296
308
  /**
@@ -302,8 +314,35 @@
302
314
  return '';
303
315
  }
304
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
+ };
305
344
  // Экранирование формул для предотвращения CSV инъекций
306
- if (preventInjection && /^[=+\-@]/.test(str)) {
345
+ if (preventInjection && isPotentialFormula(str)) {
307
346
  return "'" + str;
308
347
  }
309
348
  // Экранирование кавычек и переносов строк
@@ -312,6 +351,43 @@
312
351
  }
313
352
  return str;
314
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;
369
+ }
370
+ function normalizePhoneValue$2(value) {
371
+ const trimmed = value.trim();
372
+ if (trimmed === '') {
373
+ return trimmed;
374
+ }
375
+ return trimmed.replace(/["'\\]/g, '');
376
+ }
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;
390
+ }
315
391
  /**
316
392
  * Извлечение всех уникальных ключей из массива объектов
317
393
  * @private
@@ -344,6 +420,7 @@
344
420
  const maxRecords = options.maxRecords || data.length;
345
421
  const preventInjection = options.preventCsvInjection !== false;
346
422
  const rfc4180Compliant = options.rfc4180Compliant !== false;
423
+ const normalizeQuotes = options.normalizeQuotes !== false;
347
424
  // Ограничение количества записей
348
425
  const limitedData = data.slice(0, maxRecords);
349
426
  // Извлечение всех ключей
@@ -362,7 +439,8 @@
362
439
  for (const item of limitedData) {
363
440
  const rowValues = allKeys.map(key => {
364
441
  const value = item?.[key];
365
- return escapeCsvValue(value, preventInjection);
442
+ const normalized = normalizeValueForCsv$1(value, key, normalizeQuotes);
443
+ return escapeCsvValue(normalized, preventInjection);
366
444
  });
367
445
  lines.push(rowValues.join(delimiter));
368
446
  }
@@ -392,6 +470,7 @@
392
470
  const includeHeaders = options.includeHeaders !== false;
393
471
  const preventInjection = options.preventCsvInjection !== false;
394
472
  const rfc4180Compliant = options.rfc4180Compliant !== false;
473
+ const normalizeQuotes = options.normalizeQuotes !== false;
395
474
  let allKeys = [];
396
475
  let renameMap = {};
397
476
  // Если данные - массив, обрабатываем как массив
@@ -411,7 +490,8 @@
411
490
  for (const item of data) {
412
491
  const rowValues = allKeys.map(key => {
413
492
  const value = item?.[key];
414
- return escapeCsvValue(value, preventInjection);
493
+ const normalized = normalizeValueForCsv$1(value, key, normalizeQuotes);
494
+ return escapeCsvValue(normalized, preventInjection);
415
495
  });
416
496
  yield rowValues.join(delimiter) + (rfc4180Compliant ? '\r\n' : '\n');
417
497
  }
@@ -508,6 +588,12 @@
508
588
  if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
509
589
  throw new ConfigurationError('warnExtraFields must be a boolean');
510
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
+ }
511
597
  return true;
512
598
  }
513
599
  /**
@@ -533,6 +619,169 @@
533
619
  }
534
620
  return bestCandidate;
535
621
  }
622
+ function isEmptyValue(value) {
623
+ return value === undefined || value === null || value === '';
624
+ }
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;
636
+ }
637
+ function hasAnyQuotes(value) {
638
+ return typeof value === 'string' && value.includes('"');
639
+ }
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;
658
+ }
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;
682
+ }
683
+ function looksLikeUserAgent(value) {
684
+ if (typeof value !== 'string') {
685
+ return false;
686
+ }
687
+ return /Mozilla\/|Opera\/|MSIE|AppleWebKit|Gecko|Safari|Chrome\//.test(value);
688
+ }
689
+ function isHexColor(value) {
690
+ return typeof value === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
691
+ }
692
+ function repairShiftedRows(rows, headers, options = {}) {
693
+ if (!Array.isArray(rows) || rows.length === 0 || headers.length === 0) {
694
+ return rows;
695
+ }
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
+ }
536
785
  /**
537
786
  * Парсинг CSV строки в массив объектов
538
787
  *
@@ -559,6 +808,7 @@
559
808
  }
560
809
  // Парсинг заголовков
561
810
  const headers = lines[0].split(delimiter).map(h => h.trim());
811
+ const { repairRowShifts = true, normalizeQuotes = true } = options || {};
562
812
  // Ограничение количества строк
563
813
  const maxRows = options.maxRows || Infinity;
564
814
  const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
@@ -584,6 +834,12 @@
584
834
  }
585
835
  result.push(row);
586
836
  }
837
+ if (repairRowShifts) {
838
+ return repairShiftedRows(result, headers, { normalizeQuotes });
839
+ }
840
+ if (normalizeQuotes) {
841
+ return result.map((row) => normalizeRowQuotes(row, headers));
842
+ }
587
843
  return result;
588
844
  });
589
845
  }
@@ -625,10 +881,12 @@
625
881
  }
626
882
  // Парсинг заголовков
627
883
  const headers = lines[0].split(delimiter).map(h => h.trim());
884
+ const { repairRowShifts = true, normalizeQuotes = true } = options || {};
628
885
  // Ограничение количества строк
629
886
  const maxRows = options.maxRows || Infinity;
630
887
  const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
631
888
  // Возврат данных по одной строке
889
+ const parsedRows = [];
632
890
  for (let i = 0; i < dataRows.length; i++) {
633
891
  const line = dataRows[i];
634
892
  const values = line.split(delimiter);
@@ -636,7 +894,7 @@
636
894
  for (let j = 0; j < headers.length; j++) {
637
895
  const header = headers[j];
638
896
  const value = j < values.length ? values[j].trim() : '';
639
- // Попытка парсинга чисел
897
+ // Try parsing numbers
640
898
  if (/^-?\d+(\.\d+)?$/.test(value)) {
641
899
  row[header] = parseFloat(value);
642
900
  }
@@ -647,6 +905,14 @@
647
905
  row[header] = value;
648
906
  }
649
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) {
650
916
  yield row;
651
917
  }
652
918
  }
@@ -705,6 +971,7 @@
705
971
  parseCsvSafeAsync: parseCsvSafeAsync
706
972
  });
707
973
 
974
+ const PHONE_KEYS = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
708
975
  function isReadableStream(value) {
709
976
  return value && typeof value.getReader === 'function';
710
977
  }
@@ -775,6 +1042,39 @@
775
1042
  }
776
1043
  return 'unknown';
777
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;
1060
+ }
1061
+ function normalizePhoneValue(value) {
1062
+ const trimmed = value.trim();
1063
+ if (trimmed === '') {
1064
+ return trimmed;
1065
+ }
1066
+ return trimmed.replace(/["'\\]/g, '');
1067
+ }
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;
1077
+ }
778
1078
  async function* jsonToCsvChunkIterator(input, options = {}) {
779
1079
  const format = detectInputFormat(input, options);
780
1080
  if (format === 'csv') {
@@ -817,6 +1117,34 @@
817
1117
  const delimiter = options.delimiter || ';';
818
1118
  const includeHeaders = options.includeHeaders !== false;
819
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
+ };
820
1148
  let isFirstChunk = true;
821
1149
  let headers = [];
822
1150
  while (true) {
@@ -830,7 +1158,7 @@
830
1158
  if (includeHeaders) {
831
1159
  const headerLine = headers.map(header => {
832
1160
  const escaped = header.includes('"') ? `"${header.replace(/"/g, '""')}"` : header;
833
- return preventInjection && /^[=+\-@]/.test(escaped) ? `'${escaped}` : escaped;
1161
+ return preventInjection && isPotentialFormula(escaped) ? `'${escaped}` : escaped;
834
1162
  }).join(delimiter);
835
1163
  yield headerLine + '\n';
836
1164
  }
@@ -838,11 +1166,12 @@
838
1166
  }
839
1167
  const row = headers.map(header => {
840
1168
  const value = item[header];
841
- const strValue = value === null || value === undefined ? '' : String(value);
1169
+ const normalized = normalizeValueForCsv(value, header, normalizeQuotes);
1170
+ const strValue = normalized === null || normalized === undefined ? '' : String(normalized);
842
1171
  if (strValue.includes('"') || strValue.includes('\n') || strValue.includes('\r') || strValue.includes(delimiter)) {
843
1172
  return `"${strValue.replace(/"/g, '""')}"`;
844
1173
  }
845
- if (preventInjection && /^[=+\-@]/.test(strValue)) {
1174
+ if (preventInjection && isPotentialFormula(strValue)) {
846
1175
  return `'${strValue}`;
847
1176
  }
848
1177
  return strValue;