jtcsv 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -146
- package/bin/jtcsv.ts +280 -202
- 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 +1261 -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 +336 -7
- package/dist/jtcsv-core.cjs.js.map +1 -1
- package/dist/jtcsv-core.esm.js +336 -7
- package/dist/jtcsv-core.esm.js.map +1 -1
- package/dist/jtcsv-core.umd.js +336 -7
- package/dist/jtcsv-core.umd.js.map +1 -1
- package/dist/jtcsv-full.cjs.js +336 -7
- package/dist/jtcsv-full.cjs.js.map +1 -1
- package/dist/jtcsv-full.esm.js +336 -7
- package/dist/jtcsv-full.esm.js.map +1 -1
- package/dist/jtcsv-full.umd.js +336 -7
- package/dist/jtcsv-full.umd.js.map +1 -1
- package/dist/jtcsv-workers.esm.js +9 -0
- package/dist/jtcsv-workers.esm.js.map +1 -1
- package/dist/jtcsv-workers.umd.js +9 -0
- package/dist/jtcsv-workers.umd.js.map +1 -1
- package/dist/jtcsv.cjs.js +1998 -2092
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +1994 -2092
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +2157 -2251
- 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 +192 -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/{src → dist/src}/web-server/index.js +251 -286
- 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 +664 -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 +2 -2
- package/examples/advanced/performance-optimization.ts +2 -2
- package/examples/cli-advanced-usage.md +2 -0
- package/examples/cli-tool.ts +1 -1
- package/examples/large-dataset-example.ts +2 -2
- package/examples/simple-usage.ts +2 -2
- package/examples/streaming-example.ts +1 -1
- package/index.d.ts +186 -15
- package/package.json +243 -305
- package/plugins.d.ts +37 -0
- package/schema.d.ts +103 -0
- package/src/browser/csv-to-json-browser.ts +233 -3
- package/src/browser/errors-browser.ts +45 -28
- package/src/browser/json-to-csv-browser.ts +81 -5
- package/src/browser/streams.ts +73 -6
- package/src/core/delimiter-cache.ts +21 -11
- package/src/core/plugin-system.ts +343 -155
- package/src/core/transform-hooks.ts +20 -12
- package/src/engines/fast-path-engine.ts +48 -32
- package/src/errors.ts +1 -72
- package/src/formats/ndjson-parser.ts +6 -0
- package/src/formats/tsv-parser.ts +6 -0
- package/src/types/index.ts +21 -1
- package/src/utils/validators.ts +35 -0
- package/src/web-server/index.ts +1 -1
- package/bin/jtcsv.js +0 -2532
- package/csv-to-json.js +0 -711
- package/errors.js +0 -394
- package/examples/advanced/conditional-transformations.js +0 -446
- package/examples/advanced/csv-parser.worker.js +0 -89
- package/examples/advanced/nested-objects-example.js +0 -306
- package/examples/advanced/performance-optimization.js +0 -504
- package/examples/advanced/run-demo-server.js +0 -116
- package/examples/cli-batch-processing.js +0 -38
- package/examples/cli-tool.js +0 -183
- package/examples/error-handling.js +0 -338
- package/examples/express-api.js +0 -164
- package/examples/large-dataset-example.js +0 -182
- package/examples/ndjson-processing.js +0 -434
- package/examples/plugin-excel-exporter.js +0 -406
- package/examples/schema-validation.js +0 -640
- package/examples/simple-usage.js +0 -282
- package/examples/streaming-example.js +0 -418
- package/examples/web-workers-advanced.js +0 -28
- package/index.js +0 -82
- package/json-save.js +0 -255
- package/json-to-csv.js +0 -668
- package/plugins/README.md +0 -91
- package/plugins/express-middleware/README.md +0 -83
- package/plugins/express-middleware/example.js +0 -135
- package/plugins/express-middleware/example.ts +0 -135
- package/plugins/express-middleware/index.d.ts +0 -114
- package/plugins/express-middleware/index.js +0 -512
- package/plugins/express-middleware/index.ts +0 -557
- package/plugins/express-middleware/package.json +0 -52
- package/plugins/fastify-plugin/index.js +0 -404
- package/plugins/fastify-plugin/index.ts +0 -443
- 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/index.ts +0 -226
- 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/index.ts +0 -201
- 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/ConverterComponent.tsx +0 -386
- package/plugins/nextjs-api/examples/api-convert.js +0 -67
- package/plugins/nextjs-api/examples/api-convert.ts +0 -67
- package/plugins/nextjs-api/index.js +0 -387
- package/plugins/nextjs-api/index.tsx +0 -339
- package/plugins/nextjs-api/package.json +0 -63
- package/plugins/nextjs-api/route.js +0 -370
- package/plugins/nextjs-api/route.ts +0 -370
- package/plugins/nuxt/README.md +0 -24
- package/plugins/nuxt/index.js +0 -21
- package/plugins/nuxt/index.ts +0 -94
- package/plugins/nuxt/package.json +0 -35
- package/plugins/nuxt/runtime/composables/useJtcsv.js +0 -6
- package/plugins/nuxt/runtime/composables/useJtcsv.ts +0 -100
- package/plugins/nuxt/runtime/plugin.js +0 -6
- package/plugins/nuxt/runtime/plugin.ts +0 -71
- 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/index.ts +0 -260
- 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/index.ts +0 -301
- 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/index.ts +0 -267
- package/plugins/trpc/package.json +0 -34
- package/src/browser/browser-functions.js +0 -219
- package/src/browser/core.js +0 -92
- package/src/browser/csv-to-json-browser.js +0 -722
- package/src/browser/errors-browser.js +0 -212
- package/src/browser/extensions/plugins.js +0 -92
- package/src/browser/extensions/workers.js +0 -39
- package/src/browser/index.js +0 -113
- package/src/browser/json-to-csv-browser.js +0 -319
- package/src/browser/streams.js +0 -403
- package/src/browser/workers/csv-parser.worker.js +0 -377
- package/src/browser/workers/worker-pool.js +0 -527
- package/src/core/delimiter-cache.js +0 -200
- package/src/core/node-optimizations.js +0 -408
- package/src/core/plugin-system.js +0 -494
- package/src/core/transform-hooks.js +0 -350
- package/src/engines/fast-path-engine-new.js +0 -338
- package/src/engines/fast-path-engine.js +0 -844
- package/src/errors.js +0 -26
- package/src/formats/ndjson-parser.js +0 -467
- package/src/formats/tsv-parser.js +0 -339
- package/src/index-with-plugins.js +0 -378
- package/src/utils/bom-utils.js +0 -259
- package/src/utils/encoding-support.js +0 -124
- package/src/utils/schema-validator.js +0 -594
- package/src/utils/transform-loader.js +0 -205
- package/src/utils/zod-adapter.js +0 -170
- package/stream-csv-to-json.js +0 -560
- package/stream-json-to-csv.js +0 -465
package/dist/jtcsv-core.umd.js
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
this.name = 'JTCSVError';
|
|
16
16
|
this.code = code;
|
|
17
17
|
this.details = details;
|
|
18
|
+
this.hint = details.hint;
|
|
19
|
+
this.docs = details.docs;
|
|
20
|
+
this.context = details.context;
|
|
18
21
|
// Сохранение stack trace
|
|
19
22
|
if (Error.captureStackTrace) {
|
|
20
23
|
Error.captureStackTrace(this, JTCSVError);
|
|
@@ -210,6 +213,12 @@
|
|
|
210
213
|
if (error instanceof LimitError && error.limit && error.actual) {
|
|
211
214
|
message += ` (limit: ${error.limit}, actual: ${error.actual})`;
|
|
212
215
|
}
|
|
216
|
+
if (error.hint) {
|
|
217
|
+
message += `\nHint: ${error.hint}`;
|
|
218
|
+
}
|
|
219
|
+
if (error.docs) {
|
|
220
|
+
message += `\nDocs: ${error.docs}`;
|
|
221
|
+
}
|
|
213
222
|
}
|
|
214
223
|
if (includeStack && error.stack) {
|
|
215
224
|
message += `\n${error.stack}`;
|
|
@@ -291,6 +300,9 @@
|
|
|
291
300
|
if (options?.rfc4180Compliant !== undefined && typeof options.rfc4180Compliant !== 'boolean') {
|
|
292
301
|
throw new ConfigurationError('rfc4180Compliant must be a boolean');
|
|
293
302
|
}
|
|
303
|
+
if (options?.normalizeQuotes !== undefined && typeof options.normalizeQuotes !== 'boolean') {
|
|
304
|
+
throw new ConfigurationError('normalizeQuotes must be a boolean');
|
|
305
|
+
}
|
|
294
306
|
return true;
|
|
295
307
|
}
|
|
296
308
|
/**
|
|
@@ -302,8 +314,35 @@
|
|
|
302
314
|
return '';
|
|
303
315
|
}
|
|
304
316
|
const str = String(value);
|
|
317
|
+
const isPotentialFormula = (input) => {
|
|
318
|
+
let idx = 0;
|
|
319
|
+
while (idx < input.length) {
|
|
320
|
+
const code = input.charCodeAt(idx);
|
|
321
|
+
if (code === 32 || code === 9 || code === 10 || code === 13 || code === 0xfeff) {
|
|
322
|
+
idx++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
if (idx < input.length && (input[idx] === '"' || input[idx] === "'")) {
|
|
328
|
+
idx++;
|
|
329
|
+
while (idx < input.length) {
|
|
330
|
+
const code = input.charCodeAt(idx);
|
|
331
|
+
if (code === 32 || code === 9) {
|
|
332
|
+
idx++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (idx >= input.length) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const char = input[idx];
|
|
342
|
+
return char === '=' || char === '+' || char === '-' || char === '@';
|
|
343
|
+
};
|
|
305
344
|
// Экранирование формул для предотвращения CSV инъекций
|
|
306
|
-
if (preventInjection &&
|
|
345
|
+
if (preventInjection && isPotentialFormula(str)) {
|
|
307
346
|
return "'" + str;
|
|
308
347
|
}
|
|
309
348
|
// Экранирование кавычек и переносов строк
|
|
@@ -312,6 +351,43 @@
|
|
|
312
351
|
}
|
|
313
352
|
return str;
|
|
314
353
|
}
|
|
354
|
+
function normalizeQuotesInField$2(value) {
|
|
355
|
+
// Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
|
|
356
|
+
// Проверяем, выглядит ли значение как JSON (объект или массив)
|
|
357
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
358
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
359
|
+
return value; // Возвращаем как есть для JSON
|
|
360
|
+
}
|
|
361
|
+
let normalized = value.replace(/"{2,}/g, '"');
|
|
362
|
+
// Убираем правило, которое ломает JSON: не заменяем "," на ","
|
|
363
|
+
// normalized = normalized.replace(/"\s*,\s*"/g, ',');
|
|
364
|
+
normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
|
|
365
|
+
if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
366
|
+
normalized = normalized.slice(1, -1);
|
|
367
|
+
}
|
|
368
|
+
return normalized;
|
|
369
|
+
}
|
|
370
|
+
function normalizePhoneValue$2(value) {
|
|
371
|
+
const trimmed = value.trim();
|
|
372
|
+
if (trimmed === '') {
|
|
373
|
+
return trimmed;
|
|
374
|
+
}
|
|
375
|
+
return trimmed.replace(/["'\\]/g, '');
|
|
376
|
+
}
|
|
377
|
+
function normalizeValueForCsv$1(value, key, normalizeQuotes) {
|
|
378
|
+
if (!normalizeQuotes || typeof value !== 'string') {
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
const base = normalizeQuotesInField$2(value);
|
|
382
|
+
if (!key) {
|
|
383
|
+
return base;
|
|
384
|
+
}
|
|
385
|
+
const phoneKeys = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
|
|
386
|
+
if (phoneKeys.has(String(key).toLowerCase())) {
|
|
387
|
+
return normalizePhoneValue$2(base);
|
|
388
|
+
}
|
|
389
|
+
return base;
|
|
390
|
+
}
|
|
315
391
|
/**
|
|
316
392
|
* Извлечение всех уникальных ключей из массива объектов
|
|
317
393
|
* @private
|
|
@@ -344,6 +420,7 @@
|
|
|
344
420
|
const maxRecords = options.maxRecords || data.length;
|
|
345
421
|
const preventInjection = options.preventCsvInjection !== false;
|
|
346
422
|
const rfc4180Compliant = options.rfc4180Compliant !== false;
|
|
423
|
+
const normalizeQuotes = options.normalizeQuotes !== false;
|
|
347
424
|
// Ограничение количества записей
|
|
348
425
|
const limitedData = data.slice(0, maxRecords);
|
|
349
426
|
// Извлечение всех ключей
|
|
@@ -362,7 +439,8 @@
|
|
|
362
439
|
for (const item of limitedData) {
|
|
363
440
|
const rowValues = allKeys.map(key => {
|
|
364
441
|
const value = item?.[key];
|
|
365
|
-
|
|
442
|
+
const normalized = normalizeValueForCsv$1(value, key, normalizeQuotes);
|
|
443
|
+
return escapeCsvValue(normalized, preventInjection);
|
|
366
444
|
});
|
|
367
445
|
lines.push(rowValues.join(delimiter));
|
|
368
446
|
}
|
|
@@ -392,6 +470,7 @@
|
|
|
392
470
|
const includeHeaders = options.includeHeaders !== false;
|
|
393
471
|
const preventInjection = options.preventCsvInjection !== false;
|
|
394
472
|
const rfc4180Compliant = options.rfc4180Compliant !== false;
|
|
473
|
+
const normalizeQuotes = options.normalizeQuotes !== false;
|
|
395
474
|
let allKeys = [];
|
|
396
475
|
let renameMap = {};
|
|
397
476
|
// Если данные - массив, обрабатываем как массив
|
|
@@ -411,7 +490,8 @@
|
|
|
411
490
|
for (const item of data) {
|
|
412
491
|
const rowValues = allKeys.map(key => {
|
|
413
492
|
const value = item?.[key];
|
|
414
|
-
|
|
493
|
+
const normalized = normalizeValueForCsv$1(value, key, normalizeQuotes);
|
|
494
|
+
return escapeCsvValue(normalized, preventInjection);
|
|
415
495
|
});
|
|
416
496
|
yield rowValues.join(delimiter) + (rfc4180Compliant ? '\r\n' : '\n');
|
|
417
497
|
}
|
|
@@ -508,6 +588,12 @@
|
|
|
508
588
|
if (options?.warnExtraFields !== undefined && typeof options.warnExtraFields !== 'boolean') {
|
|
509
589
|
throw new ConfigurationError('warnExtraFields must be a boolean');
|
|
510
590
|
}
|
|
591
|
+
if (options?.repairRowShifts !== undefined && typeof options.repairRowShifts !== 'boolean') {
|
|
592
|
+
throw new ConfigurationError('repairRowShifts must be a boolean');
|
|
593
|
+
}
|
|
594
|
+
if (options?.normalizeQuotes !== undefined && typeof options.normalizeQuotes !== 'boolean') {
|
|
595
|
+
throw new ConfigurationError('normalizeQuotes must be a boolean');
|
|
596
|
+
}
|
|
511
597
|
return true;
|
|
512
598
|
}
|
|
513
599
|
/**
|
|
@@ -533,6 +619,169 @@
|
|
|
533
619
|
}
|
|
534
620
|
return bestCandidate;
|
|
535
621
|
}
|
|
622
|
+
function isEmptyValue(value) {
|
|
623
|
+
return value === undefined || value === null || value === '';
|
|
624
|
+
}
|
|
625
|
+
function hasOddQuotes(value) {
|
|
626
|
+
if (typeof value !== 'string') {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
let count = 0;
|
|
630
|
+
for (let i = 0; i < value.length; i++) {
|
|
631
|
+
if (value[i] === '"') {
|
|
632
|
+
count++;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return count % 2 === 1;
|
|
636
|
+
}
|
|
637
|
+
function hasAnyQuotes(value) {
|
|
638
|
+
return typeof value === 'string' && value.includes('"');
|
|
639
|
+
}
|
|
640
|
+
function normalizeQuotesInField$1(value) {
|
|
641
|
+
if (typeof value !== 'string') {
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
// Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
|
|
645
|
+
// Проверяем, выглядит ли значение как JSON (объект или массив)
|
|
646
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
647
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
648
|
+
return value; // Возвращаем как есть для JSON
|
|
649
|
+
}
|
|
650
|
+
let normalized = value.replace(/"{2,}/g, '"');
|
|
651
|
+
// Убираем правило, которое ломает JSON: не заменяем "," на ","
|
|
652
|
+
// normalized = normalized.replace(/"\s*,\s*"/g, ',');
|
|
653
|
+
normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
|
|
654
|
+
if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
655
|
+
normalized = normalized.slice(1, -1);
|
|
656
|
+
}
|
|
657
|
+
return normalized;
|
|
658
|
+
}
|
|
659
|
+
function normalizePhoneValue$1(value) {
|
|
660
|
+
if (typeof value !== 'string') {
|
|
661
|
+
return value;
|
|
662
|
+
}
|
|
663
|
+
const trimmed = value.trim();
|
|
664
|
+
if (trimmed === '') {
|
|
665
|
+
return trimmed;
|
|
666
|
+
}
|
|
667
|
+
return trimmed.replace(/["'\\]/g, '');
|
|
668
|
+
}
|
|
669
|
+
function normalizeRowQuotes(row, headers) {
|
|
670
|
+
const normalized = {};
|
|
671
|
+
const phoneKeys = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
|
|
672
|
+
for (const header of headers) {
|
|
673
|
+
const baseValue = normalizeQuotesInField$1(row[header]);
|
|
674
|
+
if (phoneKeys.has(String(header).toLowerCase())) {
|
|
675
|
+
normalized[header] = normalizePhoneValue$1(baseValue);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
normalized[header] = baseValue;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return normalized;
|
|
682
|
+
}
|
|
683
|
+
function looksLikeUserAgent(value) {
|
|
684
|
+
if (typeof value !== 'string') {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
return /Mozilla\/|Opera\/|MSIE|AppleWebKit|Gecko|Safari|Chrome\//.test(value);
|
|
688
|
+
}
|
|
689
|
+
function isHexColor(value) {
|
|
690
|
+
return typeof value === 'string' && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
|
691
|
+
}
|
|
692
|
+
function repairShiftedRows(rows, headers, options = {}) {
|
|
693
|
+
if (!Array.isArray(rows) || rows.length === 0 || headers.length === 0) {
|
|
694
|
+
return rows;
|
|
695
|
+
}
|
|
696
|
+
const headerCount = headers.length;
|
|
697
|
+
const merged = [];
|
|
698
|
+
let index = 0;
|
|
699
|
+
while (index < rows.length) {
|
|
700
|
+
const row = rows[index];
|
|
701
|
+
if (!row || typeof row !== 'object') {
|
|
702
|
+
merged.push(row);
|
|
703
|
+
index++;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const values = headers.map((header) => row[header]);
|
|
707
|
+
let lastNonEmpty = -1;
|
|
708
|
+
for (let i = headerCount - 1; i >= 0; i--) {
|
|
709
|
+
if (!isEmptyValue(values[i])) {
|
|
710
|
+
lastNonEmpty = i;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const missingCount = headerCount - 1 - lastNonEmpty;
|
|
715
|
+
if (lastNonEmpty >= 0 && missingCount > 0 && index + 1 < rows.length) {
|
|
716
|
+
const nextRow = rows[index + 1];
|
|
717
|
+
if (nextRow && typeof nextRow === 'object') {
|
|
718
|
+
const nextValues = headers.map((header) => nextRow[header]);
|
|
719
|
+
const nextTrailingEmpty = nextValues
|
|
720
|
+
.slice(headerCount - missingCount)
|
|
721
|
+
.every((value) => isEmptyValue(value));
|
|
722
|
+
const leadValues = nextValues
|
|
723
|
+
.slice(0, missingCount)
|
|
724
|
+
.filter((value) => !isEmptyValue(value));
|
|
725
|
+
const shouldMerge = nextTrailingEmpty
|
|
726
|
+
&& leadValues.length > 0
|
|
727
|
+
&& (hasOddQuotes(values[lastNonEmpty]) || hasAnyQuotes(values[lastNonEmpty]));
|
|
728
|
+
if (shouldMerge) {
|
|
729
|
+
const toAppend = leadValues.map((value) => String(value));
|
|
730
|
+
if (toAppend.length > 0) {
|
|
731
|
+
const base = isEmptyValue(values[lastNonEmpty]) ? '' : String(values[lastNonEmpty]);
|
|
732
|
+
values[lastNonEmpty] = base ? `${base}\n${toAppend.join('\n')}` : toAppend.join('\n');
|
|
733
|
+
}
|
|
734
|
+
for (let i = 0; i < missingCount; i++) {
|
|
735
|
+
values[lastNonEmpty + 1 + i] = nextValues[missingCount + i];
|
|
736
|
+
}
|
|
737
|
+
const mergedRow = {};
|
|
738
|
+
for (let i = 0; i < headerCount; i++) {
|
|
739
|
+
mergedRow[headers[i]] = values[i];
|
|
740
|
+
}
|
|
741
|
+
merged.push(mergedRow);
|
|
742
|
+
index += 2;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (index + 1 < rows.length && headerCount >= 6) {
|
|
748
|
+
const nextRow = rows[index + 1];
|
|
749
|
+
if (nextRow && typeof nextRow === 'object') {
|
|
750
|
+
const nextHex = nextRow[headers[4]];
|
|
751
|
+
const nextUserAgentHead = nextRow[headers[2]];
|
|
752
|
+
const nextUserAgentTail = nextRow[headers[3]];
|
|
753
|
+
const shouldMergeUserAgent = isEmptyValue(values[4])
|
|
754
|
+
&& isEmptyValue(values[5])
|
|
755
|
+
&& isHexColor(nextHex)
|
|
756
|
+
&& (looksLikeUserAgent(nextUserAgentHead) || looksLikeUserAgent(nextUserAgentTail));
|
|
757
|
+
if (shouldMergeUserAgent) {
|
|
758
|
+
const addressParts = [values[3], nextRow[headers[0]], nextRow[headers[1]]]
|
|
759
|
+
.filter((value) => !isEmptyValue(value))
|
|
760
|
+
.map((value) => String(value));
|
|
761
|
+
values[3] = addressParts.join('\n');
|
|
762
|
+
const uaHead = isEmptyValue(nextUserAgentHead) ? '' : String(nextUserAgentHead);
|
|
763
|
+
const uaTail = isEmptyValue(nextUserAgentTail) ? '' : String(nextUserAgentTail);
|
|
764
|
+
const joiner = uaHead && uaTail ? (uaTail.startsWith(' ') ? '' : ',') : '';
|
|
765
|
+
values[4] = uaHead + joiner + uaTail;
|
|
766
|
+
values[5] = String(nextHex);
|
|
767
|
+
const mergedRow = {};
|
|
768
|
+
for (let i = 0; i < headerCount; i++) {
|
|
769
|
+
mergedRow[headers[i]] = values[i];
|
|
770
|
+
}
|
|
771
|
+
merged.push(mergedRow);
|
|
772
|
+
index += 2;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
merged.push(row);
|
|
778
|
+
index++;
|
|
779
|
+
}
|
|
780
|
+
if (options.normalizeQuotes) {
|
|
781
|
+
return merged.map((row) => normalizeRowQuotes(row, headers));
|
|
782
|
+
}
|
|
783
|
+
return merged;
|
|
784
|
+
}
|
|
536
785
|
/**
|
|
537
786
|
* Парсинг CSV строки в массив объектов
|
|
538
787
|
*
|
|
@@ -559,6 +808,7 @@
|
|
|
559
808
|
}
|
|
560
809
|
// Парсинг заголовков
|
|
561
810
|
const headers = lines[0].split(delimiter).map(h => h.trim());
|
|
811
|
+
const { repairRowShifts = true, normalizeQuotes = true } = options || {};
|
|
562
812
|
// Ограничение количества строк
|
|
563
813
|
const maxRows = options.maxRows || Infinity;
|
|
564
814
|
const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
|
|
@@ -584,6 +834,12 @@
|
|
|
584
834
|
}
|
|
585
835
|
result.push(row);
|
|
586
836
|
}
|
|
837
|
+
if (repairRowShifts) {
|
|
838
|
+
return repairShiftedRows(result, headers, { normalizeQuotes });
|
|
839
|
+
}
|
|
840
|
+
if (normalizeQuotes) {
|
|
841
|
+
return result.map((row) => normalizeRowQuotes(row, headers));
|
|
842
|
+
}
|
|
587
843
|
return result;
|
|
588
844
|
});
|
|
589
845
|
}
|
|
@@ -625,10 +881,12 @@
|
|
|
625
881
|
}
|
|
626
882
|
// Парсинг заголовков
|
|
627
883
|
const headers = lines[0].split(delimiter).map(h => h.trim());
|
|
884
|
+
const { repairRowShifts = true, normalizeQuotes = true } = options || {};
|
|
628
885
|
// Ограничение количества строк
|
|
629
886
|
const maxRows = options.maxRows || Infinity;
|
|
630
887
|
const dataRows = lines.slice(1, Math.min(lines.length, maxRows + 1));
|
|
631
888
|
// Возврат данных по одной строке
|
|
889
|
+
const parsedRows = [];
|
|
632
890
|
for (let i = 0; i < dataRows.length; i++) {
|
|
633
891
|
const line = dataRows[i];
|
|
634
892
|
const values = line.split(delimiter);
|
|
@@ -636,7 +894,7 @@
|
|
|
636
894
|
for (let j = 0; j < headers.length; j++) {
|
|
637
895
|
const header = headers[j];
|
|
638
896
|
const value = j < values.length ? values[j].trim() : '';
|
|
639
|
-
//
|
|
897
|
+
// Try parsing numbers
|
|
640
898
|
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
641
899
|
row[header] = parseFloat(value);
|
|
642
900
|
}
|
|
@@ -647,6 +905,14 @@
|
|
|
647
905
|
row[header] = value;
|
|
648
906
|
}
|
|
649
907
|
}
|
|
908
|
+
parsedRows.push(row);
|
|
909
|
+
}
|
|
910
|
+
const finalRows = repairRowShifts
|
|
911
|
+
? repairShiftedRows(parsedRows, headers, { normalizeQuotes })
|
|
912
|
+
: (normalizeQuotes
|
|
913
|
+
? parsedRows.map((row) => normalizeRowQuotes(row, headers))
|
|
914
|
+
: parsedRows);
|
|
915
|
+
for (const row of finalRows) {
|
|
650
916
|
yield row;
|
|
651
917
|
}
|
|
652
918
|
}
|
|
@@ -705,6 +971,7 @@
|
|
|
705
971
|
parseCsvSafeAsync: parseCsvSafeAsync
|
|
706
972
|
});
|
|
707
973
|
|
|
974
|
+
const PHONE_KEYS = new Set(['phone', 'phonenumber', 'phone_number', 'tel', 'telephone']);
|
|
708
975
|
function isReadableStream(value) {
|
|
709
976
|
return value && typeof value.getReader === 'function';
|
|
710
977
|
}
|
|
@@ -775,6 +1042,39 @@
|
|
|
775
1042
|
}
|
|
776
1043
|
return 'unknown';
|
|
777
1044
|
}
|
|
1045
|
+
function normalizeQuotesInField(value) {
|
|
1046
|
+
// Не нормализуем кавычки в JSON-строках - это ломает структуру JSON
|
|
1047
|
+
// Проверяем, выглядит ли значение как JSON (объект или массив)
|
|
1048
|
+
if ((value.startsWith('{') && value.endsWith('}')) ||
|
|
1049
|
+
(value.startsWith('[') && value.endsWith(']'))) {
|
|
1050
|
+
return value; // Возвращаем как есть для JSON
|
|
1051
|
+
}
|
|
1052
|
+
let normalized = value.replace(/"{2,}/g, '"');
|
|
1053
|
+
// Убираем правило, которое ломает JSON: не заменяем "," на ","
|
|
1054
|
+
// normalized = normalized.replace(/"\s*,\s*"/g, ',');
|
|
1055
|
+
normalized = normalized.replace(/"\n/g, '\n').replace(/\n"/g, '\n');
|
|
1056
|
+
if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
1057
|
+
normalized = normalized.slice(1, -1);
|
|
1058
|
+
}
|
|
1059
|
+
return normalized;
|
|
1060
|
+
}
|
|
1061
|
+
function normalizePhoneValue(value) {
|
|
1062
|
+
const trimmed = value.trim();
|
|
1063
|
+
if (trimmed === '') {
|
|
1064
|
+
return trimmed;
|
|
1065
|
+
}
|
|
1066
|
+
return trimmed.replace(/["'\\]/g, '');
|
|
1067
|
+
}
|
|
1068
|
+
function normalizeValueForCsv(value, key, normalizeQuotes) {
|
|
1069
|
+
if (!normalizeQuotes || typeof value !== 'string') {
|
|
1070
|
+
return value;
|
|
1071
|
+
}
|
|
1072
|
+
const base = normalizeQuotesInField(value);
|
|
1073
|
+
if (key && PHONE_KEYS.has(String(key).toLowerCase())) {
|
|
1074
|
+
return normalizePhoneValue(base);
|
|
1075
|
+
}
|
|
1076
|
+
return base;
|
|
1077
|
+
}
|
|
778
1078
|
async function* jsonToCsvChunkIterator(input, options = {}) {
|
|
779
1079
|
const format = detectInputFormat(input, options);
|
|
780
1080
|
if (format === 'csv') {
|
|
@@ -817,6 +1117,34 @@
|
|
|
817
1117
|
const delimiter = options.delimiter || ';';
|
|
818
1118
|
const includeHeaders = options.includeHeaders !== false;
|
|
819
1119
|
const preventInjection = options.preventCsvInjection !== false;
|
|
1120
|
+
const normalizeQuotes = options.normalizeQuotes !== false;
|
|
1121
|
+
const isPotentialFormula = (input) => {
|
|
1122
|
+
let idx = 0;
|
|
1123
|
+
while (idx < input.length) {
|
|
1124
|
+
const code = input.charCodeAt(idx);
|
|
1125
|
+
if (code === 32 || code === 9 || code === 10 || code === 13 || code === 0xfeff) {
|
|
1126
|
+
idx++;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
if (idx < input.length && (input[idx] === '"' || input[idx] === "'")) {
|
|
1132
|
+
idx++;
|
|
1133
|
+
while (idx < input.length) {
|
|
1134
|
+
const code = input.charCodeAt(idx);
|
|
1135
|
+
if (code === 32 || code === 9) {
|
|
1136
|
+
idx++;
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (idx >= input.length) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
const char = input[idx];
|
|
1146
|
+
return char === '=' || char === '+' || char === '-' || char === '@';
|
|
1147
|
+
};
|
|
820
1148
|
let isFirstChunk = true;
|
|
821
1149
|
let headers = [];
|
|
822
1150
|
while (true) {
|
|
@@ -830,7 +1158,7 @@
|
|
|
830
1158
|
if (includeHeaders) {
|
|
831
1159
|
const headerLine = headers.map(header => {
|
|
832
1160
|
const escaped = header.includes('"') ? `"${header.replace(/"/g, '""')}"` : header;
|
|
833
|
-
return preventInjection &&
|
|
1161
|
+
return preventInjection && isPotentialFormula(escaped) ? `'${escaped}` : escaped;
|
|
834
1162
|
}).join(delimiter);
|
|
835
1163
|
yield headerLine + '\n';
|
|
836
1164
|
}
|
|
@@ -838,11 +1166,12 @@
|
|
|
838
1166
|
}
|
|
839
1167
|
const row = headers.map(header => {
|
|
840
1168
|
const value = item[header];
|
|
841
|
-
const
|
|
1169
|
+
const normalized = normalizeValueForCsv(value, header, normalizeQuotes);
|
|
1170
|
+
const strValue = normalized === null || normalized === undefined ? '' : String(normalized);
|
|
842
1171
|
if (strValue.includes('"') || strValue.includes('\n') || strValue.includes('\r') || strValue.includes(delimiter)) {
|
|
843
1172
|
return `"${strValue.replace(/"/g, '""')}"`;
|
|
844
1173
|
}
|
|
845
|
-
if (preventInjection &&
|
|
1174
|
+
if (preventInjection && isPotentialFormula(strValue)) {
|
|
846
1175
|
return `'${strValue}`;
|
|
847
1176
|
}
|
|
848
1177
|
return strValue;
|