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.
- package/README.md +204 -115
- package/bin/jtcsv.ts +2612 -0
- package/browser.d.ts +142 -0
- package/dist/benchmark.js +446 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/bin/jtcsv.js +1940 -0
- package/dist/bin/jtcsv.js.map +1 -0
- package/dist/csv-to-json.js +1262 -0
- package/dist/csv-to-json.js.map +1 -0
- package/dist/errors.js +291 -0
- package/dist/errors.js.map +1 -0
- package/dist/eslint.config.js +147 -0
- package/dist/eslint.config.js.map +1 -0
- package/dist/index-core.js +95 -0
- package/dist/index-core.js.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/json-save.js +229 -0
- package/dist/json-save.js.map +1 -0
- package/dist/json-to-csv.js +576 -0
- package/dist/json-to-csv.js.map +1 -0
- package/dist/jtcsv-core.cjs.js +1736 -0
- package/dist/jtcsv-core.cjs.js.map +1 -0
- package/dist/jtcsv-core.esm.js +1708 -0
- package/dist/jtcsv-core.esm.js.map +1 -0
- package/dist/jtcsv-core.umd.js +1742 -0
- package/dist/jtcsv-core.umd.js.map +1 -0
- package/dist/jtcsv-full.cjs.js +2241 -0
- package/dist/jtcsv-full.cjs.js.map +1 -0
- package/dist/jtcsv-full.esm.js +2209 -0
- package/dist/jtcsv-full.esm.js.map +1 -0
- package/dist/jtcsv-full.umd.js +2247 -0
- package/dist/jtcsv-full.umd.js.map +1 -0
- package/dist/jtcsv-workers.esm.js +768 -0
- package/dist/jtcsv-workers.esm.js.map +1 -0
- package/dist/jtcsv-workers.umd.js +782 -0
- package/dist/jtcsv-workers.umd.js.map +1 -0
- package/dist/jtcsv.cjs.js +1996 -2048
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +1992 -2048
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +2157 -2209
- package/dist/jtcsv.umd.js.map +1 -1
- package/dist/plugins/express-middleware/index.js +350 -0
- package/dist/plugins/express-middleware/index.js.map +1 -0
- package/dist/plugins/fastify-plugin/index.js +315 -0
- package/dist/plugins/fastify-plugin/index.js.map +1 -0
- package/dist/plugins/hono/index.js +111 -0
- package/dist/plugins/hono/index.js.map +1 -0
- package/dist/plugins/nestjs/index.js +112 -0
- package/dist/plugins/nestjs/index.js.map +1 -0
- package/dist/plugins/nuxt/index.js +53 -0
- package/dist/plugins/nuxt/index.js.map +1 -0
- package/dist/plugins/remix/index.js +133 -0
- package/dist/plugins/remix/index.js.map +1 -0
- package/dist/plugins/sveltekit/index.js +155 -0
- package/dist/plugins/sveltekit/index.js.map +1 -0
- package/dist/plugins/trpc/index.js +136 -0
- package/dist/plugins/trpc/index.js.map +1 -0
- package/dist/run-demo.js +49 -0
- package/dist/run-demo.js.map +1 -0
- package/dist/src/browser/browser-functions.js +193 -0
- package/dist/src/browser/browser-functions.js.map +1 -0
- package/dist/src/browser/core.js +123 -0
- package/dist/src/browser/core.js.map +1 -0
- package/dist/src/browser/csv-to-json-browser.js +353 -0
- package/dist/src/browser/csv-to-json-browser.js.map +1 -0
- package/dist/src/browser/errors-browser.js +219 -0
- package/dist/src/browser/errors-browser.js.map +1 -0
- package/dist/src/browser/extensions/plugins.js +106 -0
- package/dist/src/browser/extensions/plugins.js.map +1 -0
- package/dist/src/browser/extensions/workers.js +66 -0
- package/dist/src/browser/extensions/workers.js.map +1 -0
- package/dist/src/browser/index.js +140 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/json-to-csv-browser.js +225 -0
- package/dist/src/browser/json-to-csv-browser.js.map +1 -0
- package/dist/src/browser/streams.js +340 -0
- package/dist/src/browser/streams.js.map +1 -0
- package/dist/src/browser/workers/csv-parser.worker.js +264 -0
- package/dist/src/browser/workers/csv-parser.worker.js.map +1 -0
- package/dist/src/browser/workers/worker-pool.js +338 -0
- package/dist/src/browser/workers/worker-pool.js.map +1 -0
- package/dist/src/core/delimiter-cache.js +196 -0
- package/dist/src/core/delimiter-cache.js.map +1 -0
- package/dist/src/core/node-optimizations.js +279 -0
- package/dist/src/core/node-optimizations.js.map +1 -0
- package/dist/src/core/plugin-system.js +399 -0
- package/dist/src/core/plugin-system.js.map +1 -0
- package/dist/src/core/transform-hooks.js +348 -0
- package/dist/src/core/transform-hooks.js.map +1 -0
- package/dist/src/engines/fast-path-engine-new.js +262 -0
- package/dist/src/engines/fast-path-engine-new.js.map +1 -0
- package/dist/src/engines/fast-path-engine.js +671 -0
- package/dist/src/engines/fast-path-engine.js.map +1 -0
- package/dist/src/errors.js +18 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/formats/ndjson-parser.js +332 -0
- package/dist/src/formats/ndjson-parser.js.map +1 -0
- package/dist/src/formats/tsv-parser.js +230 -0
- package/dist/src/formats/tsv-parser.js.map +1 -0
- package/dist/src/index-with-plugins.js +259 -0
- package/dist/src/index-with-plugins.js.map +1 -0
- package/dist/src/types/index.js +3 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/utils/bom-utils.js +267 -0
- package/dist/src/utils/bom-utils.js.map +1 -0
- package/dist/src/utils/encoding-support.js +77 -0
- package/dist/src/utils/encoding-support.js.map +1 -0
- package/dist/src/utils/schema-validator.js +609 -0
- package/dist/src/utils/schema-validator.js.map +1 -0
- package/dist/src/utils/transform-loader.js +281 -0
- package/dist/src/utils/transform-loader.js.map +1 -0
- package/dist/src/utils/validators.js +40 -0
- package/dist/src/utils/validators.js.map +1 -0
- package/dist/src/utils/zod-adapter.js +144 -0
- package/dist/src/utils/zod-adapter.js.map +1 -0
- package/dist/src/web-server/index.js +648 -0
- package/dist/src/web-server/index.js.map +1 -0
- package/dist/src/workers/csv-multithreaded.js +211 -0
- package/dist/src/workers/csv-multithreaded.js.map +1 -0
- package/dist/src/workers/csv-parser.worker.js +179 -0
- package/dist/src/workers/csv-parser.worker.js.map +1 -0
- package/dist/src/workers/worker-pool.js +228 -0
- package/dist/src/workers/worker-pool.js.map +1 -0
- package/dist/stream-csv-to-json.js +665 -0
- package/dist/stream-csv-to-json.js.map +1 -0
- package/dist/stream-json-to-csv.js +389 -0
- package/dist/stream-json-to-csv.js.map +1 -0
- package/examples/advanced/conditional-transformations.ts +446 -0
- package/examples/advanced/csv-parser.worker.ts +89 -0
- package/examples/advanced/nested-objects-example.ts +306 -0
- package/examples/advanced/performance-optimization.ts +504 -0
- package/examples/advanced/run-demo-server.ts +116 -0
- package/examples/advanced/web-worker-usage.html +874 -0
- package/examples/async-multithreaded-example.ts +335 -0
- package/examples/cli-advanced-usage.md +290 -0
- package/examples/{cli-batch-processing.js → cli-batch-processing.ts} +38 -38
- package/examples/{cli-tool.js → cli-tool.ts} +5 -8
- package/examples/{error-handling.js → error-handling.ts} +356 -324
- package/examples/{express-api.js → express-api.ts} +161 -164
- package/examples/{large-dataset-example.js → large-dataset-example.ts} +201 -182
- package/examples/{ndjson-processing.js → ndjson-processing.ts} +456 -434
- package/examples/{plugin-excel-exporter.js → plugin-excel-exporter.ts} +6 -7
- package/examples/react-integration.tsx +637 -0
- package/examples/{schema-validation.js → schema-validation.ts} +2 -2
- package/examples/simple-usage.ts +194 -0
- package/examples/{streaming-example.js → streaming-example.ts} +12 -12
- package/index.d.ts +187 -18
- package/package.json +75 -81
- package/plugins.d.ts +37 -0
- package/schema.d.ts +103 -0
- package/src/browser/browser-functions.ts +402 -0
- package/src/browser/core.ts +152 -0
- package/src/browser/csv-to-json-browser.d.ts +3 -0
- package/src/browser/csv-to-json-browser.ts +494 -0
- package/src/browser/{errors-browser.js → errors-browser.ts} +305 -197
- package/src/browser/extensions/plugins.ts +93 -0
- package/src/browser/extensions/workers.ts +39 -0
- package/src/browser/globals.d.ts +5 -0
- package/src/browser/index.ts +192 -0
- package/src/browser/json-to-csv-browser.d.ts +3 -0
- package/src/browser/json-to-csv-browser.ts +338 -0
- package/src/browser/streams.ts +403 -0
- package/src/browser/workers/{csv-parser.worker.js → csv-parser.worker.ts} +3 -3
- package/src/browser/workers/{worker-pool.js → worker-pool.ts} +51 -30
- package/src/core/delimiter-cache.ts +320 -0
- package/src/core/{node-optimizations.js → node-optimizations.ts} +448 -407
- package/src/core/plugin-system.ts +588 -0
- package/src/core/transform-hooks.ts +566 -0
- package/src/engines/{fast-path-engine-new.js → fast-path-engine-new.ts} +11 -2
- package/src/engines/{fast-path-engine.js → fast-path-engine.ts} +79 -53
- package/src/errors.ts +1 -0
- package/src/formats/{ndjson-parser.js → ndjson-parser.ts} +24 -16
- package/src/formats/{tsv-parser.js → tsv-parser.ts} +18 -17
- package/src/{index-with-plugins.js → index-with-plugins.ts} +381 -357
- package/src/types/index.ts +275 -0
- package/src/utils/bom-utils.ts +373 -0
- package/src/utils/encoding-support.ts +155 -0
- package/src/utils/{schema-validator.js → schema-validator.ts} +814 -589
- package/src/utils/transform-loader.ts +389 -0
- package/src/utils/validators.ts +35 -0
- package/src/utils/zod-adapter.ts +280 -0
- package/src/web-server/{index.js → index.ts} +19 -19
- package/src/workers/csv-multithreaded.ts +310 -0
- package/src/workers/csv-parser.worker.ts +227 -0
- package/src/workers/worker-pool.ts +409 -0
- package/bin/jtcsv.js +0 -2462
- package/csv-to-json.js +0 -688
- package/errors.js +0 -208
- package/examples/simple-usage.js +0 -282
- package/index.js +0 -68
- package/json-save.js +0 -254
- package/json-to-csv.js +0 -526
- package/plugins/README.md +0 -91
- package/plugins/express-middleware/README.md +0 -64
- package/plugins/express-middleware/example.js +0 -136
- package/plugins/express-middleware/index.d.ts +0 -114
- package/plugins/express-middleware/index.js +0 -360
- package/plugins/express-middleware/package.json +0 -52
- package/plugins/fastify-plugin/index.js +0 -406
- package/plugins/fastify-plugin/package.json +0 -55
- package/plugins/hono/README.md +0 -28
- package/plugins/hono/index.d.ts +0 -12
- package/plugins/hono/index.js +0 -36
- package/plugins/hono/package.json +0 -35
- package/plugins/nestjs/README.md +0 -35
- package/plugins/nestjs/index.d.ts +0 -25
- package/plugins/nestjs/index.js +0 -77
- package/plugins/nestjs/package.json +0 -37
- package/plugins/nextjs-api/README.md +0 -57
- package/plugins/nextjs-api/examples/ConverterComponent.jsx +0 -386
- package/plugins/nextjs-api/examples/api-convert.js +0 -69
- package/plugins/nextjs-api/index.js +0 -387
- package/plugins/nextjs-api/package.json +0 -63
- package/plugins/nextjs-api/route.js +0 -371
- package/plugins/nuxt/README.md +0 -24
- package/plugins/nuxt/index.js +0 -21
- package/plugins/nuxt/package.json +0 -35
- package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
- package/plugins/nuxt/runtime/plugin.js +0 -6
- package/plugins/remix/README.md +0 -26
- package/plugins/remix/index.d.ts +0 -16
- package/plugins/remix/index.js +0 -62
- package/plugins/remix/package.json +0 -35
- package/plugins/sveltekit/README.md +0 -28
- package/plugins/sveltekit/index.d.ts +0 -17
- package/plugins/sveltekit/index.js +0 -54
- package/plugins/sveltekit/package.json +0 -33
- package/plugins/trpc/README.md +0 -25
- package/plugins/trpc/index.d.ts +0 -7
- package/plugins/trpc/index.js +0 -32
- package/plugins/trpc/package.json +0 -34
- package/src/browser/browser-functions.js +0 -219
- package/src/browser/csv-to-json-browser.js +0 -700
- package/src/browser/index.js +0 -113
- package/src/browser/json-to-csv-browser.js +0 -309
- package/src/browser/streams.js +0 -393
- package/src/core/delimiter-cache.js +0 -186
- package/src/core/plugin-system.js +0 -476
- package/src/core/transform-hooks.js +0 -350
- package/src/errors.js +0 -26
- package/src/utils/transform-loader.js +0 -205
- package/stream-csv-to-json.js +0 -542
- package/stream-json-to-csv.js +0 -464
- /package/examples/{web-workers-advanced.js → web-workers-advanced.ts} +0 -0
|
@@ -1,700 +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 && /^-?\d+(\.\d+)?$/.test(result)) {
|
|
195
|
-
const num = parseFloat(result);
|
|
196
|
-
if (!isNaN(num)) {
|
|
197
|
-
return num;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Парсинг булевых значений
|
|
202
|
-
if (parseBooleans) {
|
|
203
|
-
const lowerValue = result.toLowerCase();
|
|
204
|
-
if (lowerValue === 'true') {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
if (lowerValue === 'false') {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Пустые строки как null
|
|
213
|
-
if (result === '') {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function isSimpleCsv(csv) {
|
|
221
|
-
return csv.indexOf('"') === -1 && csv.indexOf('\\') === -1;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function parseSimpleCsv(csv, delimiter, options) {
|
|
225
|
-
const {
|
|
226
|
-
hasHeaders = true,
|
|
227
|
-
renameMap = {},
|
|
228
|
-
trim = true,
|
|
229
|
-
parseNumbers = false,
|
|
230
|
-
parseBooleans = false,
|
|
231
|
-
maxRows
|
|
232
|
-
} = options;
|
|
233
|
-
|
|
234
|
-
const result = [];
|
|
235
|
-
let headers = null;
|
|
236
|
-
let fieldStart = 0;
|
|
237
|
-
let currentRow = [];
|
|
238
|
-
let rowHasData = false;
|
|
239
|
-
let rowCount = 0;
|
|
240
|
-
|
|
241
|
-
const finalizeRow = (fields) => {
|
|
242
|
-
if (fields.length === 1 && fields[0].trim() === '') {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!headers) {
|
|
247
|
-
if (hasHeaders) {
|
|
248
|
-
headers = fields.map(header => {
|
|
249
|
-
const trimmed = trim ? header.trim() : header;
|
|
250
|
-
return renameMap[trimmed] || trimmed;
|
|
251
|
-
});
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
headers = fields.map((_, index) => `column${index + 1}`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
rowCount++;
|
|
259
|
-
if (maxRows && rowCount > maxRows) {
|
|
260
|
-
throw new LimitError(
|
|
261
|
-
`CSV size exceeds maximum limit of ${maxRows} rows`,
|
|
262
|
-
maxRows,
|
|
263
|
-
rowCount
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const row = {};
|
|
268
|
-
const fieldCount = Math.min(fields.length, headers.length);
|
|
269
|
-
for (let i = 0; i < fieldCount; i++) {
|
|
270
|
-
row[headers[i]] = parseCsvValue(fields[i], { trim, parseNumbers, parseBooleans });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
result.push(row);
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
let i = 0;
|
|
277
|
-
while (i <= csv.length) {
|
|
278
|
-
const char = i < csv.length ? csv[i] : '\n';
|
|
279
|
-
|
|
280
|
-
if (char !== '\r' && char !== '\n' && char !== ' ' && char !== '\t') {
|
|
281
|
-
rowHasData = true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (char === delimiter || char === '\n' || char === '\r' || i === csv.length) {
|
|
285
|
-
const field = csv.slice(fieldStart, i);
|
|
286
|
-
currentRow.push(field);
|
|
287
|
-
|
|
288
|
-
if (char === '\n' || char === '\r' || i === csv.length) {
|
|
289
|
-
if (rowHasData || currentRow.length > 1) {
|
|
290
|
-
finalizeRow(currentRow);
|
|
291
|
-
}
|
|
292
|
-
currentRow = [];
|
|
293
|
-
rowHasData = false;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (char === '\r' && csv[i + 1] === '\n') {
|
|
297
|
-
i++;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
fieldStart = i + 1;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
i++;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Автоматическое определение разделителя CSV
|
|
311
|
-
*
|
|
312
|
-
* @param {string} csv - CSV строка
|
|
313
|
-
* @param {Array} [candidates=[';', ',', '\t', '|']] - Кандидаты на разделитель
|
|
314
|
-
* @returns {string} Определенный разделитель
|
|
315
|
-
*/
|
|
316
|
-
export function autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|']) {
|
|
317
|
-
if (!csv || typeof csv !== 'string') {
|
|
318
|
-
return ';'; // значение по умолчанию
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const lines = csv.split('\n').filter(line => line.trim().length > 0);
|
|
322
|
-
|
|
323
|
-
if (lines.length === 0) {
|
|
324
|
-
return ';'; // значение по умолчанию
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Использование первой непустой строки для определения
|
|
328
|
-
const firstLine = lines[0];
|
|
329
|
-
|
|
330
|
-
const counts = {};
|
|
331
|
-
candidates.forEach(delim => {
|
|
332
|
-
const escapedDelim = delim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
333
|
-
const regex = new RegExp(escapedDelim, 'g');
|
|
334
|
-
const matches = firstLine.match(regex);
|
|
335
|
-
counts[delim] = matches ? matches.length : 0;
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// Поиск разделителя с максимальным количеством
|
|
339
|
-
let maxCount = -1;
|
|
340
|
-
let detectedDelimiter = ';'; // значение по умолчанию
|
|
341
|
-
|
|
342
|
-
for (const [delim, count] of Object.entries(counts)) {
|
|
343
|
-
if (count > maxCount) {
|
|
344
|
-
maxCount = count;
|
|
345
|
-
detectedDelimiter = delim;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Если разделитель не найден или ничья
|
|
350
|
-
if (maxCount === 0) {
|
|
351
|
-
return ';'; // значение по умолчанию
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return detectedDelimiter;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Конвертирует CSV строку в JSON массив
|
|
359
|
-
*
|
|
360
|
-
* @param {string} csv - CSV строка для конвертации
|
|
361
|
-
* @param {Object} [options] - Опции конфигурации
|
|
362
|
-
* @param {string} [options.delimiter] - CSV разделитель (по умолчанию: автоопределение)
|
|
363
|
-
* @param {boolean} [options.autoDetect=true] - Автоопределение разделителя
|
|
364
|
-
* @param {Array} [options.candidates=[';', ',', '\t', '|']] - Кандидаты для автоопределения
|
|
365
|
-
* @param {boolean} [options.hasHeaders=true] - Есть ли заголовки в CSV
|
|
366
|
-
* @param {Object} [options.renameMap={}] - Маппинг переименования заголовков
|
|
367
|
-
* @param {boolean} [options.trim=true] - Обрезать пробелы
|
|
368
|
-
* @param {boolean} [options.parseNumbers=false] - Парсить числовые значения
|
|
369
|
-
* @param {boolean} [options.parseBooleans=false] - Парсить булевы значения
|
|
370
|
-
* @param {number} [options.maxRows] - Максимальное количество строк
|
|
371
|
-
* @returns {Array<Object>} JSON массив
|
|
372
|
-
*/
|
|
373
|
-
export function csvToJson(csv, options = {}) {
|
|
374
|
-
return safeExecute(() => {
|
|
375
|
-
// Валидация ввода
|
|
376
|
-
validateCsvInput(csv, options);
|
|
377
|
-
|
|
378
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
379
|
-
|
|
380
|
-
const {
|
|
381
|
-
delimiter,
|
|
382
|
-
autoDetect = true,
|
|
383
|
-
candidates = [';', ',', '\t', '|'],
|
|
384
|
-
hasHeaders = true,
|
|
385
|
-
renameMap = {},
|
|
386
|
-
trim = true,
|
|
387
|
-
parseNumbers = false,
|
|
388
|
-
parseBooleans = false,
|
|
389
|
-
maxRows,
|
|
390
|
-
warnExtraFields = true
|
|
391
|
-
} = opts;
|
|
392
|
-
|
|
393
|
-
// Определение разделителя
|
|
394
|
-
let finalDelimiter = delimiter;
|
|
395
|
-
if (!finalDelimiter && autoDetect) {
|
|
396
|
-
finalDelimiter = autoDetectDelimiter(csv, candidates);
|
|
397
|
-
}
|
|
398
|
-
finalDelimiter = finalDelimiter || ';'; // fallback
|
|
399
|
-
|
|
400
|
-
// Обработка пустого CSV
|
|
401
|
-
if (csv.trim() === '') {
|
|
402
|
-
return [];
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (isSimpleCsv(csv)) {
|
|
406
|
-
return parseSimpleCsv(csv, finalDelimiter, {
|
|
407
|
-
hasHeaders,
|
|
408
|
-
renameMap,
|
|
409
|
-
trim,
|
|
410
|
-
parseNumbers,
|
|
411
|
-
parseBooleans,
|
|
412
|
-
maxRows
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Парсинг CSV с обработкой кавычек и переносов строк
|
|
417
|
-
const lines = [];
|
|
418
|
-
let currentLine = '';
|
|
419
|
-
let insideQuotes = false;
|
|
420
|
-
|
|
421
|
-
for (let i = 0; i < csv.length; i++) {
|
|
422
|
-
const char = csv[i];
|
|
423
|
-
|
|
424
|
-
if (char === '"') {
|
|
425
|
-
if (insideQuotes && i + 1 < csv.length && csv[i + 1] === '"') {
|
|
426
|
-
// Экранированная кавычка внутри кавычек
|
|
427
|
-
currentLine += '"';
|
|
428
|
-
i++; // Пропустить следующую кавычку
|
|
429
|
-
} else {
|
|
430
|
-
// Переключение режима кавычек
|
|
431
|
-
insideQuotes = !insideQuotes;
|
|
432
|
-
}
|
|
433
|
-
currentLine += char;
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (char === '\n' && !insideQuotes) {
|
|
438
|
-
// Конец строки (вне кавычек)
|
|
439
|
-
lines.push(currentLine);
|
|
440
|
-
currentLine = '';
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (char === '\r') {
|
|
445
|
-
// Игнорировать carriage return
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
currentLine += char;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Добавление последней строки
|
|
453
|
-
if (currentLine !== '' || insideQuotes) {
|
|
454
|
-
lines.push(currentLine);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (lines.length === 0) {
|
|
458
|
-
return [];
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Предупреждение для больших наборов данных
|
|
462
|
-
if (lines.length > 1000000 && !maxRows && process.env.NODE_ENV !== 'production') {
|
|
463
|
-
console.warn(
|
|
464
|
-
'⚠️ Warning: Processing >1M records in memory may be slow.\n' +
|
|
465
|
-
'💡 Consider using Web Workers for better performance with large files.\n' +
|
|
466
|
-
'📊 Current size: ' + lines.length.toLocaleString() + ' rows'
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Применение ограничения по строкам
|
|
471
|
-
if (maxRows && lines.length > maxRows) {
|
|
472
|
-
throw new LimitError(
|
|
473
|
-
`CSV size exceeds maximum limit of ${maxRows} rows`,
|
|
474
|
-
maxRows,
|
|
475
|
-
lines.length
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
let headers = [];
|
|
480
|
-
let startIndex = 0;
|
|
481
|
-
|
|
482
|
-
// Парсинг заголовков если есть
|
|
483
|
-
if (hasHeaders && lines.length > 0) {
|
|
484
|
-
try {
|
|
485
|
-
headers = parseCsvLine(lines[0], 1, finalDelimiter).map(header => {
|
|
486
|
-
const trimmed = trim ? header.trim() : header;
|
|
487
|
-
return renameMap[trimmed] || trimmed;
|
|
488
|
-
});
|
|
489
|
-
startIndex = 1;
|
|
490
|
-
} catch (error) {
|
|
491
|
-
if (error instanceof ParsingError) {
|
|
492
|
-
throw new ParsingError(`Failed to parse headers: ${error.message}`, 1);
|
|
493
|
-
}
|
|
494
|
-
throw error;
|
|
495
|
-
}
|
|
496
|
-
} else {
|
|
497
|
-
// Генерация числовых заголовков из первой строки
|
|
498
|
-
try {
|
|
499
|
-
const firstLineFields = parseCsvLine(lines[0], 1, finalDelimiter);
|
|
500
|
-
headers = firstLineFields.map((_, index) => `column${index + 1}`);
|
|
501
|
-
} catch (error) {
|
|
502
|
-
if (error instanceof ParsingError) {
|
|
503
|
-
throw new ParsingError(`Failed to parse first line: ${error.message}`, 1);
|
|
504
|
-
}
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Парсинг строк данных
|
|
510
|
-
const result = [];
|
|
511
|
-
|
|
512
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
513
|
-
const line = lines[i];
|
|
514
|
-
|
|
515
|
-
// Пропуск пустых строк
|
|
516
|
-
if (line.trim() === '') {
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
const fields = parseCsvLine(line, i + 1, finalDelimiter);
|
|
522
|
-
|
|
523
|
-
// Обработка несоответствия количества полей
|
|
524
|
-
const row = {};
|
|
525
|
-
const fieldCount = Math.min(fields.length, headers.length);
|
|
526
|
-
|
|
527
|
-
for (let j = 0; j < fieldCount; j++) {
|
|
528
|
-
row[headers[j]] = parseCsvValue(fields[j], { trim, parseNumbers, parseBooleans });
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Предупреждение о лишних полях
|
|
532
|
-
const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development';
|
|
533
|
-
if (fields.length > headers.length && warnExtraFields && isDev) {
|
|
534
|
-
console.warn(`[jtcsv] Line ${i + 1}: ${fields.length - headers.length} extra fields ignored`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
result.push(row);
|
|
538
|
-
} catch (error) {
|
|
539
|
-
if (error instanceof ParsingError) {
|
|
540
|
-
throw new ParsingError(`Line ${i + 1}: ${error.message}`, i + 1);
|
|
541
|
-
}
|
|
542
|
-
throw error;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return result;
|
|
547
|
-
}, 'PARSE_FAILED', { function: 'csvToJson' });
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
export async function* csvToJsonIterator(input, options = {}) {
|
|
551
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
552
|
-
validateCsvOptions(opts);
|
|
553
|
-
|
|
554
|
-
if (typeof input === 'string') {
|
|
555
|
-
const rows = csvToJson(input, options);
|
|
556
|
-
for (const row of rows) {
|
|
557
|
-
yield row;
|
|
558
|
-
}
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const {
|
|
563
|
-
delimiter,
|
|
564
|
-
autoDetect = true,
|
|
565
|
-
candidates = [';', ',', '\t', '|'],
|
|
566
|
-
hasHeaders = true,
|
|
567
|
-
renameMap = {},
|
|
568
|
-
trim = true,
|
|
569
|
-
parseNumbers = false,
|
|
570
|
-
parseBooleans = false,
|
|
571
|
-
maxRows
|
|
572
|
-
} = opts;
|
|
573
|
-
|
|
574
|
-
const stream = (input instanceof Blob && input.stream) ? input.stream() : input;
|
|
575
|
-
if (!stream || typeof stream.getReader !== 'function') {
|
|
576
|
-
throw new ValidationError('Input must be a CSV string, Blob/File, or ReadableStream');
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const reader = stream.getReader();
|
|
580
|
-
const decoder = new TextDecoder('utf-8');
|
|
581
|
-
let buffer = '';
|
|
582
|
-
let insideQuotes = false;
|
|
583
|
-
let headers = null;
|
|
584
|
-
let rowCount = 0;
|
|
585
|
-
let lineNumber = 0;
|
|
586
|
-
let finalDelimiter = delimiter;
|
|
587
|
-
let delimiterResolved = Boolean(finalDelimiter);
|
|
588
|
-
|
|
589
|
-
const processFields = (fields) => {
|
|
590
|
-
if (fields.length === 1 && fields[0].trim() === '') {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
rowCount++;
|
|
595
|
-
if (maxRows && rowCount > maxRows) {
|
|
596
|
-
throw new LimitError(
|
|
597
|
-
`CSV size exceeds maximum limit of ${maxRows} rows`,
|
|
598
|
-
maxRows,
|
|
599
|
-
rowCount
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const row = {};
|
|
604
|
-
const fieldCount = Math.min(fields.length, headers.length);
|
|
605
|
-
for (let j = 0; j < fieldCount; j++) {
|
|
606
|
-
row[headers[j]] = parseCsvValue(fields[j], { trim, parseNumbers, parseBooleans });
|
|
607
|
-
}
|
|
608
|
-
return row;
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
const processLine = (line) => {
|
|
612
|
-
lineNumber++;
|
|
613
|
-
let cleanLine = line;
|
|
614
|
-
if (cleanLine.endsWith('\r')) {
|
|
615
|
-
cleanLine = cleanLine.slice(0, -1);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (!delimiterResolved) {
|
|
619
|
-
if (!finalDelimiter && autoDetect) {
|
|
620
|
-
finalDelimiter = autoDetectDelimiter(cleanLine, candidates);
|
|
621
|
-
}
|
|
622
|
-
finalDelimiter = finalDelimiter || ';';
|
|
623
|
-
delimiterResolved = true;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if (cleanLine.trim() === '') {
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (!headers) {
|
|
631
|
-
if (hasHeaders) {
|
|
632
|
-
headers = parseCsvLine(cleanLine, lineNumber, finalDelimiter).map(header => {
|
|
633
|
-
const trimmed = trim ? header.trim() : header;
|
|
634
|
-
return renameMap[trimmed] || trimmed;
|
|
635
|
-
});
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
|
|
640
|
-
headers = fields.map((_, index) => `column${index + 1}`);
|
|
641
|
-
return processFields(fields);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const fields = parseCsvLine(cleanLine, lineNumber, finalDelimiter);
|
|
645
|
-
return processFields(fields);
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
while (true) {
|
|
649
|
-
const { value, done } = await reader.read();
|
|
650
|
-
if (done) {
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
buffer += decoder.decode(value, { stream: true });
|
|
655
|
-
|
|
656
|
-
let start = 0;
|
|
657
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
658
|
-
const char = buffer[i];
|
|
659
|
-
if (char === '"') {
|
|
660
|
-
if (insideQuotes && buffer[i + 1] === '"') {
|
|
661
|
-
i++;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
insideQuotes = !insideQuotes;
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (char === '\n' && !insideQuotes) {
|
|
669
|
-
const line = buffer.slice(start, i);
|
|
670
|
-
start = i + 1;
|
|
671
|
-
const row = processLine(line);
|
|
672
|
-
if (row) {
|
|
673
|
-
yield row;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
buffer = buffer.slice(start);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (buffer.length > 0) {
|
|
682
|
-
const row = processLine(buffer);
|
|
683
|
-
if (row) {
|
|
684
|
-
yield row;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (insideQuotes) {
|
|
689
|
-
throw new ParsingError('Unclosed quotes in CSV', lineNumber);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// Экспорт для Node.js совместимости
|
|
694
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
695
|
-
module.exports = {
|
|
696
|
-
csvToJson,
|
|
697
|
-
autoDetectDelimiter,
|
|
698
|
-
csvToJsonIterator
|
|
699
|
-
};
|
|
700
|
-
}
|