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