jtcsv 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +205 -146
  2. package/bin/jtcsv.ts +280 -202
  3. package/browser.d.ts +142 -0
  4. package/dist/benchmark.js +446 -0
  5. package/dist/benchmark.js.map +1 -0
  6. package/dist/bin/jtcsv.js +1940 -0
  7. package/dist/bin/jtcsv.js.map +1 -0
  8. package/dist/csv-to-json.js +1262 -0
  9. package/dist/csv-to-json.js.map +1 -0
  10. package/dist/errors.js +291 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/eslint.config.js +147 -0
  13. package/dist/eslint.config.js.map +1 -0
  14. package/dist/index-core.js +95 -0
  15. package/dist/index-core.js.map +1 -0
  16. package/dist/index.js +93 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/json-save.js +229 -0
  19. package/dist/json-save.js.map +1 -0
  20. package/dist/json-to-csv.js +576 -0
  21. package/dist/json-to-csv.js.map +1 -0
  22. package/dist/jtcsv-core.cjs.js +336 -7
  23. package/dist/jtcsv-core.cjs.js.map +1 -1
  24. package/dist/jtcsv-core.esm.js +336 -7
  25. package/dist/jtcsv-core.esm.js.map +1 -1
  26. package/dist/jtcsv-core.umd.js +336 -7
  27. package/dist/jtcsv-core.umd.js.map +1 -1
  28. package/dist/jtcsv-full.cjs.js +336 -7
  29. package/dist/jtcsv-full.cjs.js.map +1 -1
  30. package/dist/jtcsv-full.esm.js +336 -7
  31. package/dist/jtcsv-full.esm.js.map +1 -1
  32. package/dist/jtcsv-full.umd.js +336 -7
  33. package/dist/jtcsv-full.umd.js.map +1 -1
  34. package/dist/jtcsv-workers.esm.js +9 -0
  35. package/dist/jtcsv-workers.esm.js.map +1 -1
  36. package/dist/jtcsv-workers.umd.js +9 -0
  37. package/dist/jtcsv-workers.umd.js.map +1 -1
  38. package/dist/jtcsv.cjs.js +1998 -2092
  39. package/dist/jtcsv.cjs.js.map +1 -1
  40. package/dist/jtcsv.esm.js +1994 -2092
  41. package/dist/jtcsv.esm.js.map +1 -1
  42. package/dist/jtcsv.umd.js +2157 -2251
  43. package/dist/jtcsv.umd.js.map +1 -1
  44. package/dist/plugins/express-middleware/index.js +350 -0
  45. package/dist/plugins/express-middleware/index.js.map +1 -0
  46. package/dist/plugins/fastify-plugin/index.js +315 -0
  47. package/dist/plugins/fastify-plugin/index.js.map +1 -0
  48. package/dist/plugins/hono/index.js +111 -0
  49. package/dist/plugins/hono/index.js.map +1 -0
  50. package/dist/plugins/nestjs/index.js +112 -0
  51. package/dist/plugins/nestjs/index.js.map +1 -0
  52. package/dist/plugins/nuxt/index.js +53 -0
  53. package/dist/plugins/nuxt/index.js.map +1 -0
  54. package/dist/plugins/remix/index.js +133 -0
  55. package/dist/plugins/remix/index.js.map +1 -0
  56. package/dist/plugins/sveltekit/index.js +155 -0
  57. package/dist/plugins/sveltekit/index.js.map +1 -0
  58. package/dist/plugins/trpc/index.js +136 -0
  59. package/dist/plugins/trpc/index.js.map +1 -0
  60. package/dist/run-demo.js +49 -0
  61. package/dist/run-demo.js.map +1 -0
  62. package/dist/src/browser/browser-functions.js +193 -0
  63. package/dist/src/browser/browser-functions.js.map +1 -0
  64. package/dist/src/browser/core.js +123 -0
  65. package/dist/src/browser/core.js.map +1 -0
  66. package/dist/src/browser/csv-to-json-browser.js +353 -0
  67. package/dist/src/browser/csv-to-json-browser.js.map +1 -0
  68. package/dist/src/browser/errors-browser.js +219 -0
  69. package/dist/src/browser/errors-browser.js.map +1 -0
  70. package/dist/src/browser/extensions/plugins.js +106 -0
  71. package/dist/src/browser/extensions/plugins.js.map +1 -0
  72. package/dist/src/browser/extensions/workers.js +66 -0
  73. package/dist/src/browser/extensions/workers.js.map +1 -0
  74. package/dist/src/browser/index.js +140 -0
  75. package/dist/src/browser/index.js.map +1 -0
  76. package/dist/src/browser/json-to-csv-browser.js +225 -0
  77. package/dist/src/browser/json-to-csv-browser.js.map +1 -0
  78. package/dist/src/browser/streams.js +340 -0
  79. package/dist/src/browser/streams.js.map +1 -0
  80. package/dist/src/browser/workers/csv-parser.worker.js +264 -0
  81. package/dist/src/browser/workers/csv-parser.worker.js.map +1 -0
  82. package/dist/src/browser/workers/worker-pool.js +338 -0
  83. package/dist/src/browser/workers/worker-pool.js.map +1 -0
  84. package/dist/src/core/delimiter-cache.js +196 -0
  85. package/dist/src/core/delimiter-cache.js.map +1 -0
  86. package/dist/src/core/node-optimizations.js +279 -0
  87. package/dist/src/core/node-optimizations.js.map +1 -0
  88. package/dist/src/core/plugin-system.js +399 -0
  89. package/dist/src/core/plugin-system.js.map +1 -0
  90. package/dist/src/core/transform-hooks.js +348 -0
  91. package/dist/src/core/transform-hooks.js.map +1 -0
  92. package/dist/src/engines/fast-path-engine-new.js +262 -0
  93. package/dist/src/engines/fast-path-engine-new.js.map +1 -0
  94. package/dist/src/engines/fast-path-engine.js +671 -0
  95. package/dist/src/engines/fast-path-engine.js.map +1 -0
  96. package/dist/src/errors.js +18 -0
  97. package/dist/src/errors.js.map +1 -0
  98. package/dist/src/formats/ndjson-parser.js +332 -0
  99. package/dist/src/formats/ndjson-parser.js.map +1 -0
  100. package/dist/src/formats/tsv-parser.js +230 -0
  101. package/dist/src/formats/tsv-parser.js.map +1 -0
  102. package/dist/src/index-with-plugins.js +259 -0
  103. package/dist/src/index-with-plugins.js.map +1 -0
  104. package/dist/src/types/index.js +3 -0
  105. package/dist/src/types/index.js.map +1 -0
  106. package/dist/src/utils/bom-utils.js +267 -0
  107. package/dist/src/utils/bom-utils.js.map +1 -0
  108. package/dist/src/utils/encoding-support.js +77 -0
  109. package/dist/src/utils/encoding-support.js.map +1 -0
  110. package/dist/src/utils/schema-validator.js +609 -0
  111. package/dist/src/utils/schema-validator.js.map +1 -0
  112. package/dist/src/utils/transform-loader.js +281 -0
  113. package/dist/src/utils/transform-loader.js.map +1 -0
  114. package/dist/src/utils/validators.js +40 -0
  115. package/dist/src/utils/validators.js.map +1 -0
  116. package/dist/src/utils/zod-adapter.js +144 -0
  117. package/dist/src/utils/zod-adapter.js.map +1 -0
  118. package/{src → dist/src}/web-server/index.js +251 -286
  119. package/dist/src/web-server/index.js.map +1 -0
  120. package/dist/src/workers/csv-multithreaded.js +211 -0
  121. package/dist/src/workers/csv-multithreaded.js.map +1 -0
  122. package/dist/src/workers/csv-parser.worker.js +179 -0
  123. package/dist/src/workers/csv-parser.worker.js.map +1 -0
  124. package/dist/src/workers/worker-pool.js +228 -0
  125. package/dist/src/workers/worker-pool.js.map +1 -0
  126. package/dist/stream-csv-to-json.js +665 -0
  127. package/dist/stream-csv-to-json.js.map +1 -0
  128. package/dist/stream-json-to-csv.js +389 -0
  129. package/dist/stream-json-to-csv.js.map +1 -0
  130. package/examples/advanced/conditional-transformations.ts +2 -2
  131. package/examples/advanced/performance-optimization.ts +2 -2
  132. package/examples/cli-advanced-usage.md +2 -0
  133. package/examples/cli-tool.ts +1 -1
  134. package/examples/large-dataset-example.ts +2 -2
  135. package/examples/simple-usage.ts +2 -2
  136. package/examples/streaming-example.ts +1 -1
  137. package/index.d.ts +186 -15
  138. package/package.json +43 -108
  139. package/plugins.d.ts +37 -0
  140. package/schema.d.ts +103 -0
  141. package/src/browser/csv-to-json-browser.ts +233 -3
  142. package/src/browser/errors-browser.ts +45 -28
  143. package/src/browser/json-to-csv-browser.ts +81 -5
  144. package/src/browser/streams.ts +73 -6
  145. package/src/core/delimiter-cache.ts +21 -11
  146. package/src/core/plugin-system.ts +343 -155
  147. package/src/core/transform-hooks.ts +20 -12
  148. package/src/engines/fast-path-engine.ts +48 -32
  149. package/src/errors.ts +1 -72
  150. package/src/formats/ndjson-parser.ts +6 -0
  151. package/src/formats/tsv-parser.ts +6 -0
  152. package/src/types/index.ts +21 -1
  153. package/src/utils/validators.ts +35 -0
  154. package/src/web-server/index.ts +1 -1
  155. package/bin/jtcsv.js +0 -2532
  156. package/csv-to-json.js +0 -711
  157. package/errors.js +0 -394
  158. package/examples/advanced/conditional-transformations.js +0 -446
  159. package/examples/advanced/csv-parser.worker.js +0 -89
  160. package/examples/advanced/nested-objects-example.js +0 -306
  161. package/examples/advanced/performance-optimization.js +0 -504
  162. package/examples/advanced/run-demo-server.js +0 -116
  163. package/examples/cli-batch-processing.js +0 -38
  164. package/examples/cli-tool.js +0 -183
  165. package/examples/error-handling.js +0 -338
  166. package/examples/express-api.js +0 -164
  167. package/examples/large-dataset-example.js +0 -182
  168. package/examples/ndjson-processing.js +0 -434
  169. package/examples/plugin-excel-exporter.js +0 -406
  170. package/examples/schema-validation.js +0 -640
  171. package/examples/simple-usage.js +0 -282
  172. package/examples/streaming-example.js +0 -418
  173. package/examples/web-workers-advanced.js +0 -28
  174. package/index.js +0 -82
  175. package/json-save.js +0 -255
  176. package/json-to-csv.js +0 -668
  177. package/plugins/README.md +0 -91
  178. package/plugins/express-middleware/README.md +0 -83
  179. package/plugins/express-middleware/example.js +0 -135
  180. package/plugins/express-middleware/example.ts +0 -135
  181. package/plugins/express-middleware/index.d.ts +0 -114
  182. package/plugins/express-middleware/index.js +0 -512
  183. package/plugins/express-middleware/index.ts +0 -557
  184. package/plugins/express-middleware/package.json +0 -52
  185. package/plugins/fastify-plugin/index.js +0 -404
  186. package/plugins/fastify-plugin/index.ts +0 -443
  187. package/plugins/fastify-plugin/package.json +0 -55
  188. package/plugins/hono/README.md +0 -28
  189. package/plugins/hono/index.d.ts +0 -12
  190. package/plugins/hono/index.js +0 -36
  191. package/plugins/hono/index.ts +0 -226
  192. package/plugins/hono/package.json +0 -35
  193. package/plugins/nestjs/README.md +0 -35
  194. package/plugins/nestjs/index.d.ts +0 -25
  195. package/plugins/nestjs/index.js +0 -77
  196. package/plugins/nestjs/index.ts +0 -201
  197. package/plugins/nestjs/package.json +0 -37
  198. package/plugins/nextjs-api/README.md +0 -57
  199. package/plugins/nextjs-api/examples/ConverterComponent.jsx +0 -386
  200. package/plugins/nextjs-api/examples/ConverterComponent.tsx +0 -386
  201. package/plugins/nextjs-api/examples/api-convert.js +0 -67
  202. package/plugins/nextjs-api/examples/api-convert.ts +0 -67
  203. package/plugins/nextjs-api/index.js +0 -387
  204. package/plugins/nextjs-api/index.tsx +0 -339
  205. package/plugins/nextjs-api/package.json +0 -63
  206. package/plugins/nextjs-api/route.js +0 -370
  207. package/plugins/nextjs-api/route.ts +0 -370
  208. package/plugins/nuxt/README.md +0 -24
  209. package/plugins/nuxt/index.js +0 -21
  210. package/plugins/nuxt/index.ts +0 -94
  211. package/plugins/nuxt/package.json +0 -35
  212. package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
  213. package/plugins/nuxt/runtime/composables/useJtcsv.ts +0 -100
  214. package/plugins/nuxt/runtime/plugin.js +0 -6
  215. package/plugins/nuxt/runtime/plugin.ts +0 -71
  216. package/plugins/remix/README.md +0 -26
  217. package/plugins/remix/index.d.ts +0 -16
  218. package/plugins/remix/index.js +0 -62
  219. package/plugins/remix/index.ts +0 -260
  220. package/plugins/remix/package.json +0 -35
  221. package/plugins/sveltekit/README.md +0 -28
  222. package/plugins/sveltekit/index.d.ts +0 -17
  223. package/plugins/sveltekit/index.js +0 -54
  224. package/plugins/sveltekit/index.ts +0 -301
  225. package/plugins/sveltekit/package.json +0 -33
  226. package/plugins/trpc/README.md +0 -25
  227. package/plugins/trpc/index.d.ts +0 -7
  228. package/plugins/trpc/index.js +0 -32
  229. package/plugins/trpc/index.ts +0 -267
  230. package/plugins/trpc/package.json +0 -34
  231. package/src/browser/browser-functions.js +0 -219
  232. package/src/browser/core.js +0 -92
  233. package/src/browser/csv-to-json-browser.js +0 -722
  234. package/src/browser/errors-browser.js +0 -212
  235. package/src/browser/extensions/plugins.js +0 -92
  236. package/src/browser/extensions/workers.js +0 -39
  237. package/src/browser/index.js +0 -113
  238. package/src/browser/json-to-csv-browser.js +0 -319
  239. package/src/browser/streams.js +0 -403
  240. package/src/browser/workers/csv-parser.worker.js +0 -377
  241. package/src/browser/workers/worker-pool.js +0 -527
  242. package/src/core/delimiter-cache.js +0 -200
  243. package/src/core/node-optimizations.js +0 -408
  244. package/src/core/plugin-system.js +0 -494
  245. package/src/core/transform-hooks.js +0 -350
  246. package/src/engines/fast-path-engine-new.js +0 -338
  247. package/src/engines/fast-path-engine.js +0 -844
  248. package/src/errors.js +0 -26
  249. package/src/formats/ndjson-parser.js +0 -467
  250. package/src/formats/tsv-parser.js +0 -339
  251. package/src/index-with-plugins.js +0 -378
  252. package/src/utils/bom-utils.js +0 -259
  253. package/src/utils/encoding-support.js +0 -124
  254. package/src/utils/schema-validator.js +0 -594
  255. package/src/utils/transform-loader.js +0 -205
  256. package/src/utils/zod-adapter.js +0 -170
  257. package/stream-csv-to-json.js +0 -560
  258. package/stream-json-to-csv.js +0 -465
@@ -1,722 +0,0 @@
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.js';
11
-
12
- /**
13
- * Валидация опций парсинга
14
- * @private
15
- */
16
- function validateCsvOptions(options) {
17
- // Validate options
18
- if (options && typeof options !== 'object') {
19
- throw new ConfigurationError('Options must be an object');
20
- }
21
-
22
- // Validate delimiter
23
- if (options?.delimiter && typeof options.delimiter !== 'string') {
24
- throw new ConfigurationError('Delimiter must be a string');
25
- }
26
-
27
- if (options?.delimiter && options.delimiter.length !== 1) {
28
- throw new ConfigurationError('Delimiter must be a single character');
29
- }
30
-
31
- // Validate autoDetect
32
- if (options?.autoDetect !== undefined && typeof options.autoDetect !== 'boolean') {
33
- throw new ConfigurationError('autoDetect must be a boolean');
34
- }
35
-
36
- // Validate candidates
37
- if (options?.candidates && !Array.isArray(options.candidates)) {
38
- throw new ConfigurationError('candidates must be an array');
39
- }
40
-
41
- // Validate maxRows
42
- if (options?.maxRows !== undefined && (typeof options.maxRows !== 'number' || options.maxRows <= 0)) {
43
- throw new ConfigurationError('maxRows must be a positive number');
44
- }
45
-
46
- if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
47
- throw new ConfigurationError('warnExtraFields must be a boolean');
48
- }
49
-
50
- return true;
51
- }
52
-
53
- /**
54
- * Валидация CSV ввода и опций
55
- * @private
56
- */
57
- function validateCsvInput(csv, options) {
58
- // Validate CSV input
59
- if (typeof csv !== 'string') {
60
- throw new ValidationError('Input must be a CSV string');
61
- }
62
-
63
- return validateCsvOptions(options);
64
- }
65
-
66
- /**
67
- * Парсинг одной строки CSV с правильным экранированием
68
- * @private
69
- */
70
- function parseCsvLine(line, lineNumber, delimiter) {
71
- const fields = [];
72
- let currentField = '';
73
- let insideQuotes = false;
74
- let escapeNext = false;
75
-
76
- for (let i = 0; i < line.length; i++) {
77
- const char = line[i];
78
-
79
- if (escapeNext) {
80
- currentField += char;
81
- escapeNext = false;
82
- continue;
83
- }
84
-
85
- if (char === '\\') {
86
- if (i + 1 === line.length) {
87
- // Обратный слеш в конце строки
88
- currentField += char;
89
- } else if (line[i + 1] === '\\') {
90
- // Двойной обратный слеш
91
- currentField += char;
92
- i++; // Пропустить следующий слеш
93
- } else {
94
- // Экранирование следующего символа
95
- escapeNext = true;
96
- }
97
- continue;
98
- }
99
-
100
- if (char === '"') {
101
- if (insideQuotes) {
102
- if (i + 1 < line.length && line[i + 1] === '"') {
103
- // Экранированная кавычка внутри кавычек
104
- currentField += '"';
105
- i++; // Пропустить следующую кавычку
106
-
107
- // Проверка конца поля
108
- let isEndOfField = false;
109
- let j = i + 1;
110
- while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
111
- j++;
112
- }
113
- if (j === line.length || line[j] === delimiter) {
114
- isEndOfField = true;
115
- }
116
-
117
- if (isEndOfField) {
118
- insideQuotes = false;
119
- }
120
- } else {
121
- // Проверка конца поля
122
- let isEndOfField = false;
123
- let j = i + 1;
124
- while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
125
- j++;
126
- }
127
- if (j === line.length || line[j] === delimiter) {
128
- isEndOfField = true;
129
- }
130
-
131
- if (isEndOfField) {
132
- insideQuotes = false;
133
- } else {
134
- currentField += '"';
135
- }
136
- }
137
- } else {
138
- // Начало поля в кавычках
139
- insideQuotes = true;
140
- }
141
- continue;
142
- }
143
-
144
- if (!insideQuotes && char === delimiter) {
145
- // Конец поля
146
- fields.push(currentField);
147
- currentField = '';
148
- continue;
149
- }
150
-
151
- currentField += char;
152
- }
153
-
154
- // Обработка незавершенного экранирования
155
- if (escapeNext) {
156
- currentField += '\\';
157
- }
158
-
159
- // Добавление последнего поля
160
- fields.push(currentField);
161
-
162
- // Проверка незакрытых кавычек
163
- if (insideQuotes) {
164
- throw new ParsingError('Unclosed quotes in CSV', lineNumber);
165
- }
166
-
167
- // Валидация количества полей
168
- if (fields.length === 0) {
169
- throw new ParsingError('No fields found', lineNumber);
170
- }
171
-
172
- return fields;
173
- }
174
-
175
- /**
176
- * Парсинг значения на основе опций
177
- * @private
178
- */
179
- function parseCsvValue(value, options) {
180
- const { trim = true, parseNumbers = false, parseBooleans = false } = options;
181
-
182
- let result = value;
183
-
184
- if (trim) {
185
- result = result.trim();
186
- }
187
-
188
- // Удаление защиты формул Excel
189
- if (result.startsWith("'")) {
190
- result = result.substring(1);
191
- }
192
-
193
- // Парсинг чисел
194
- if (parseNumbers) {
195
- // Быстрая проверка числа: первый символ цифра, минус или точка
196
- const trimmed = result.trim();
197
- const firstChar = trimmed.charAt(0);
198
- if ((firstChar >= '0' && firstChar <= '9') || firstChar === '-' || firstChar === '.') {
199
- const num = parseFloat(trimmed);
200
- if (!isNaN(num) && isFinite(num)) {
201
- // Убедимся, что строка полностью соответствует числу (без лишних символов)
202
- if (String(num) === trimmed || (trimmed.includes('.') && !isNaN(Number(trimmed)))) {
203
- return num;
204
- }
205
- }
206
- }
207
- }
208
-
209
- // Парсинг булевых значений
210
- if (parseBooleans) {
211
- const lowerValue = result.toLowerCase();
212
- if (lowerValue === 'true') {
213
- return true;
214
- }
215
- if (lowerValue === 'false') {
216
- return false;
217
- }
218
- }
219
-
220
- // Пустые строки как null
221
- if (result === '') {
222
- return null;
223
- }
224
-
225
- return result;
226
- }
227
-
228
- function isSimpleCsv(csv) {
229
- return csv.indexOf('"') === -1 && csv.indexOf('\\') === -1;
230
- }
231
-
232
- function parseSimpleCsv(csv, delimiter, options) {
233
- const {
234
- hasHeaders = true,
235
- renameMap = {},
236
- trim = true,
237
- parseNumbers = false,
238
- parseBooleans = false,
239
- maxRows
240
- } = options;
241
-
242
- const result = [];
243
- let headers = null;
244
- let fieldStart = 0;
245
- let currentRow = [];
246
- let rowHasData = false;
247
- let rowCount = 0;
248
-
249
- const finalizeRow = (fields) => {
250
- if (fields.length === 1 && fields[0].trim() === '') {
251
- return;
252
- }
253
-
254
- if (!headers) {
255
- if (hasHeaders) {
256
- headers = fields.map(header => {
257
- const trimmed = trim ? header.trim() : header;
258
- return renameMap[trimmed] || trimmed;
259
- });
260
- return;
261
- }
262
-
263
- headers = fields.map((_, index) => `column${index + 1}`);
264
- }
265
-
266
- rowCount++;
267
- if (maxRows && rowCount > maxRows) {
268
- throw new LimitError(
269
- `CSV size exceeds maximum limit of ${maxRows} rows`,
270
- maxRows,
271
- rowCount
272
- );
273
- }
274
-
275
- const row = {};
276
- const fieldCount = Math.min(fields.length, headers.length);
277
- for (let i = 0; i < fieldCount; i++) {
278
- row[headers[i]] = parseCsvValue(fields[i], { trim, parseNumbers, parseBooleans });
279
- }
280
-
281
- result.push(row);
282
- };
283
-
284
- let i = 0;
285
- while (i <= csv.length) {
286
- const char = i < csv.length ? csv[i] : '\n';
287
-
288
- if (char !== '\r' && char !== '\n' && char !== ' ' && char !== '\t') {
289
- rowHasData = true;
290
- }
291
-
292
- if (char === delimiter || char === '\n' || char === '\r' || i === csv.length) {
293
- const field = csv.slice(fieldStart, i);
294
- currentRow.push(field);
295
-
296
- if (char === '\n' || char === '\r' || i === csv.length) {
297
- if (rowHasData || currentRow.length > 1) {
298
- finalizeRow(currentRow);
299
- }
300
- currentRow = [];
301
- rowHasData = false;
302
- }
303
-
304
- if (char === '\r' && csv[i + 1] === '\n') {
305
- i++;
306
- }
307
-
308
- fieldStart = i + 1;
309
- }
310
-
311
- i++;
312
- }
313
-
314
- return result;
315
- }
316
-
317
- /**
318
- * Автоматическое определение разделителя CSV
319
- *
320
- * @param {string} csv - CSV строка
321
- * @param {Array} [candidates=[';', ',', '\t', '|']] - Кандидаты на разделитель
322
- * @returns {string} Определенный разделитель
323
- */
324
- export function autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|']) {
325
- if (!csv || typeof csv !== 'string') {
326
- return ';'; // значение по умолчанию
327
- }
328
-
329
- const lines = csv.split('\n').filter(line => line.trim().length > 0);
330
-
331
- if (lines.length === 0) {
332
- return ';'; // значение по умолчанию
333
- }
334
-
335
- // Использование первой непустой строки для определения
336
- const firstLine = lines[0];
337
-
338
- // Быстрый подсчёт вхождений кандидатов за один проход
339
- const counts = {};
340
- const candidateSet = new Set(candidates);
341
- for (let i = 0; i < firstLine.length; i++) {
342
- const char = firstLine[i];
343
- if (candidateSet.has(char)) {
344
- counts[char] = (counts[char] || 0) + 1;
345
- }
346
- }
347
- // Убедимся, что все кандидаты присутствуют в counts (даже с нулём)
348
- for (const delim of candidates) {
349
- if (!(delim in counts)) {
350
- counts[delim] = 0;
351
- }
352
- }
353
-
354
- // Поиск разделителя с максимальным количеством
355
- let maxCount = -1;
356
- let detectedDelimiter = ';'; // значение по умолчанию
357
- const maxDelimiters = [];
358
-
359
- for (const [delim, count] of Object.entries(counts)) {
360
- if (count > maxCount) {
361
- maxCount = count;
362
- maxDelimiters.length = 0;
363
- maxDelimiters.push(delim);
364
- } else if (count === maxCount) {
365
- maxDelimiters.push(delim);
366
- }
367
- }
368
-
369
- // Если разделитель не найден или есть ничья, возвращаем стандартный
370
- if (maxCount === 0 || maxDelimiters.length > 1) {
371
- detectedDelimiter = ';';
372
- } else {
373
- detectedDelimiter = maxDelimiters[0];
374
- }
375
-
376
- return detectedDelimiter;
377
- }
378
-
379
- /**
380
- * Конвертирует CSV строку в JSON массив
381
- *
382
- * @param {string} csv - CSV строка для конвертации
383
- * @param {Object} [options] - Опции конфигурации
384
- * @param {string} [options.delimiter] - CSV разделитель (по умолчанию: автоопределение)
385
- * @param {boolean} [options.autoDetect=true] - Автоопределение разделителя
386
- * @param {Array} [options.candidates=[';', ',', '\t', '|']] - Кандидаты для автоопределения
387
- * @param {boolean} [options.hasHeaders=true] - Есть ли заголовки в CSV
388
- * @param {Object} [options.renameMap={}] - Маппинг переименования заголовков
389
- * @param {boolean} [options.trim=true] - Обрезать пробелы
390
- * @param {boolean} [options.parseNumbers=false] - Парсить числовые значения
391
- * @param {boolean} [options.parseBooleans=false] - Парсить булевы значения
392
- * @param {number} [options.maxRows] - Максимальное количество строк
393
- * @returns {Array<Object>} JSON массив
394
- */
395
- export function csvToJson(csv, options = {}) {
396
- return safeExecute(() => {
397
- // Валидация ввода
398
- validateCsvInput(csv, options);
399
-
400
- const opts = options && typeof options === 'object' ? options : {};
401
-
402
- const {
403
- delimiter,
404
- autoDetect = true,
405
- candidates = [';', ',', '\t', '|'],
406
- hasHeaders = true,
407
- renameMap = {},
408
- trim = true,
409
- parseNumbers = false,
410
- parseBooleans = false,
411
- maxRows,
412
- warnExtraFields = true
413
- } = opts;
414
-
415
- // Определение разделителя
416
- let finalDelimiter = delimiter;
417
- if (!finalDelimiter && autoDetect) {
418
- finalDelimiter = autoDetectDelimiter(csv, candidates);
419
- }
420
- finalDelimiter = finalDelimiter || ';'; // fallback
421
-
422
- // Обработка пустого CSV
423
- if (csv.trim() === '') {
424
- return [];
425
- }
426
-
427
- if (isSimpleCsv(csv)) {
428
- return parseSimpleCsv(csv, finalDelimiter, {
429
- hasHeaders,
430
- renameMap,
431
- trim,
432
- parseNumbers,
433
- parseBooleans,
434
- maxRows
435
- });
436
- }
437
-
438
- // Парсинг CSV с обработкой кавычек и переносов строк
439
- const lines = [];
440
- let currentLine = '';
441
- let insideQuotes = false;
442
-
443
- for (let i = 0; i < csv.length; i++) {
444
- const char = csv[i];
445
-
446
- if (char === '"') {
447
- if (insideQuotes && i + 1 < csv.length && csv[i + 1] === '"') {
448
- // Экранированная кавычка внутри кавычек
449
- currentLine += '"';
450
- i++; // Пропустить следующую кавычку
451
- } else {
452
- // Переключение режима кавычек
453
- insideQuotes = !insideQuotes;
454
- }
455
- currentLine += char;
456
- continue;
457
- }
458
-
459
- if (char === '\n' && !insideQuotes) {
460
- // Конец строки (вне кавычек)
461
- lines.push(currentLine);
462
- currentLine = '';
463
- continue;
464
- }
465
-
466
- if (char === '\r') {
467
- // Игнорировать carriage return
468
- continue;
469
- }
470
-
471
- currentLine += char;
472
- }
473
-
474
- // Добавление последней строки
475
- if (currentLine !== '' || insideQuotes) {
476
- lines.push(currentLine);
477
- }
478
-
479
- if (lines.length === 0) {
480
- return [];
481
- }
482
-
483
- // Предупреждение для больших наборов данных
484
- if (lines.length > 1000000 && !maxRows && process.env.NODE_ENV !== 'production') {
485
- console.warn(
486
- '⚠️ Warning: Processing >1M records in memory may be slow.\n' +
487
- '💡 Consider using Web Workers for better performance with large files.\n' +
488
- '📊 Current size: ' + lines.length.toLocaleString() + ' rows'
489
- );
490
- }
491
-
492
- // Применение ограничения по строкам
493
- if (maxRows && lines.length > maxRows) {
494
- throw new LimitError(
495
- `CSV size exceeds maximum limit of ${maxRows} rows`,
496
- maxRows,
497
- lines.length
498
- );
499
- }
500
-
501
- let headers = [];
502
- let startIndex = 0;
503
-
504
- // Парсинг заголовков если есть
505
- if (hasHeaders && lines.length > 0) {
506
- try {
507
- headers = parseCsvLine(lines[0], 1, finalDelimiter).map(header => {
508
- const trimmed = trim ? header.trim() : header;
509
- return renameMap[trimmed] || trimmed;
510
- });
511
- startIndex = 1;
512
- } catch (error) {
513
- if (error instanceof ParsingError) {
514
- throw new ParsingError(`Failed to parse headers: ${error.message}`, 1);
515
- }
516
- throw error;
517
- }
518
- } else {
519
- // Генерация числовых заголовков из первой строки
520
- try {
521
- const firstLineFields = parseCsvLine(lines[0], 1, finalDelimiter);
522
- headers = firstLineFields.map((_, index) => `column${index + 1}`);
523
- } catch (error) {
524
- if (error instanceof ParsingError) {
525
- throw new ParsingError(`Failed to parse first line: ${error.message}`, 1);
526
- }
527
- throw error;
528
- }
529
- }
530
-
531
- // Парсинг строк данных
532
- const result = [];
533
-
534
- for (let i = startIndex; i < lines.length; i++) {
535
- const line = lines[i];
536
-
537
- // Пропуск пустых строк
538
- if (line.trim() === '') {
539
- continue;
540
- }
541
-
542
- try {
543
- const fields = parseCsvLine(line, i + 1, finalDelimiter);
544
-
545
- // Обработка несоответствия количества полей
546
- const row = {};
547
- const fieldCount = Math.min(fields.length, headers.length);
548
-
549
- for (let j = 0; j < fieldCount; j++) {
550
- row[headers[j]] = parseCsvValue(fields[j], { trim, parseNumbers, parseBooleans });
551
- }
552
-
553
- // Предупреждение о лишних полях
554
- const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development';
555
- if (fields.length > headers.length && warnExtraFields && isDev) {
556
- console.warn(`[jtcsv] Line ${i + 1}: ${fields.length - headers.length} extra fields ignored`);
557
- }
558
-
559
- result.push(row);
560
- } catch (error) {
561
- if (error instanceof ParsingError) {
562
- throw new ParsingError(`Line ${i + 1}: ${error.message}`, i + 1);
563
- }
564
- throw error;
565
- }
566
- }
567
-
568
- return result;
569
- }, 'PARSE_FAILED', { function: 'csvToJson' });
570
- }
571
-
572
- export async function* csvToJsonIterator(input, options = {}) {
573
- const opts = options && typeof options === 'object' ? options : {};
574
- validateCsvOptions(opts);
575
-
576
- if (typeof input === 'string') {
577
- const rows = csvToJson(input, options);
578
- for (const row of rows) {
579
- yield row;
580
- }
581
- return;
582
- }
583
-
584
- const {
585
- delimiter,
586
- autoDetect = true,
587
- candidates = [';', ',', '\t', '|'],
588
- hasHeaders = true,
589
- renameMap = {},
590
- trim = true,
591
- parseNumbers = false,
592
- parseBooleans = false,
593
- maxRows
594
- } = opts;
595
-
596
- const stream = (input instanceof Blob && input.stream) ? input.stream() : input;
597
- if (!stream || typeof stream.getReader !== 'function') {
598
- throw new ValidationError('Input must be a CSV string, Blob/File, or ReadableStream');
599
- }
600
-
601
- const reader = stream.getReader();
602
- const decoder = new TextDecoder('utf-8');
603
- let buffer = '';
604
- let insideQuotes = false;
605
- let headers = null;
606
- let rowCount = 0;
607
- let lineNumber = 0;
608
- let finalDelimiter = delimiter;
609
- let delimiterResolved = Boolean(finalDelimiter);
610
-
611
- const processFields = (fields) => {
612
- if (fields.length === 1 && fields[0].trim() === '') {
613
- return null;
614
- }
615
-
616
- rowCount++;
617
- if (maxRows && rowCount > maxRows) {
618
- throw new LimitError(
619
- `CSV size exceeds maximum limit of ${maxRows} rows`,
620
- maxRows,
621
- rowCount
622
- );
623
- }
624
-
625
- const row = {};
626
- const fieldCount = Math.min(fields.length, headers.length);
627
- for (let j = 0; j < fieldCount; j++) {
628
- row[headers[j]] = parseCsvValue(fields[j], { trim, parseNumbers, parseBooleans });
629
- }
630
- return row;
631
- };
632
-
633
- const processLine = (line) => {
634
- lineNumber++;
635
- let cleanLine = line;
636
- if (cleanLine.endsWith('\r')) {
637
- cleanLine = cleanLine.slice(0, -1);
638
- }
639
-
640
- if (!delimiterResolved) {
641
- if (!finalDelimiter && autoDetect) {
642
- finalDelimiter = autoDetectDelimiter(cleanLine, candidates);
643
- }
644
- finalDelimiter = finalDelimiter || ';';
645
- delimiterResolved = true;
646
- }
647
-
648
- if (cleanLine.trim() === '') {
649
- return null;
650
- }
651
-
652
- if (!headers) {
653
- if (hasHeaders) {
654
- headers = parseCsvLine(cleanLine, lineNumber, finalDelimiter).map(header => {
655
- const trimmed = trim ? header.trim() : header;
656
- return renameMap[trimmed] || trimmed;
657
- });
658
- return null;
659
- }
660
-
661
- const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
662
- headers = fields.map((_, index) => `column${index + 1}`);
663
- return processFields(fields);
664
- }
665
-
666
- const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
667
- return processFields(fields);
668
- };
669
-
670
- while (true) {
671
- const { value, done } = await reader.read();
672
- if (done) {
673
- break;
674
- }
675
-
676
- buffer += decoder.decode(value, { stream: true });
677
-
678
- let start = 0;
679
- for (let i = 0; i < buffer.length; i++) {
680
- const char = buffer[i];
681
- if (char === '"') {
682
- if (insideQuotes && buffer[i + 1] === '"') {
683
- i++;
684
- continue;
685
- }
686
- insideQuotes = !insideQuotes;
687
- continue;
688
- }
689
-
690
- if (char === '\n' && !insideQuotes) {
691
- const line = buffer.slice(start, i);
692
- start = i + 1;
693
- const row = processLine(line);
694
- if (row) {
695
- yield row;
696
- }
697
- }
698
- }
699
-
700
- buffer = buffer.slice(start);
701
- }
702
-
703
- if (buffer.length > 0) {
704
- const row = processLine(buffer);
705
- if (row) {
706
- yield row;
707
- }
708
- }
709
-
710
- if (insideQuotes) {
711
- throw new ParsingError('Unclosed quotes in CSV', lineNumber);
712
- }
713
- }
714
-
715
- // Экспорт для Node.js совместимости
716
- if (typeof module !== 'undefined' && module.exports) {
717
- module.exports = {
718
- csvToJson,
719
- autoDetectDelimiter,
720
- csvToJsonIterator
721
- };
722
- }