jtcsv 2.1.0 → 2.1.3
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 +63 -17
- package/bin/jtcsv.js +1013 -117
- package/csv-to-json.js +385 -311
- package/examples/simple-usage.js +2 -3
- package/index.d.ts +288 -5
- package/index.js +23 -0
- package/json-to-csv.js +130 -89
- package/package.json +47 -19
- package/plugins/README.md +146 -2
- package/plugins/hono/README.md +25 -0
- package/plugins/hono/index.d.ts +12 -0
- package/plugins/hono/index.js +36 -0
- package/plugins/hono/package.json +35 -0
- package/plugins/nestjs/README.md +33 -0
- package/plugins/nestjs/index.d.ts +25 -0
- package/plugins/nestjs/index.js +77 -0
- package/plugins/nestjs/package.json +37 -0
- package/plugins/nuxt/README.md +25 -0
- package/plugins/nuxt/index.js +21 -0
- package/plugins/nuxt/package.json +35 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.js +6 -0
- package/plugins/nuxt/runtime/plugin.js +6 -0
- package/plugins/remix/README.md +26 -0
- package/plugins/remix/index.d.ts +16 -0
- package/plugins/remix/index.js +62 -0
- package/plugins/remix/package.json +35 -0
- package/plugins/sveltekit/README.md +28 -0
- package/plugins/sveltekit/index.d.ts +17 -0
- package/plugins/sveltekit/index.js +54 -0
- package/plugins/sveltekit/package.json +33 -0
- package/plugins/trpc/README.md +22 -0
- package/plugins/trpc/index.d.ts +7 -0
- package/plugins/trpc/index.js +32 -0
- package/plugins/trpc/package.json +34 -0
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine.js +829 -340
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +36 -14
- package/cli-tui.js +0 -5
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSV (Tab-Separated Values) парсер
|
|
3
|
+
* Специализированная поддержка TSV формата
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-23
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { csvToJson } = require('../../csv-to-json');
|
|
10
|
+
const { jsonToCsv } = require('../../json-to-csv');
|
|
11
|
+
const { ValidationError, SecurityError, FileSystemError } = require('../../errors');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function validateTsvFilePath(filePath) {
|
|
15
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
16
|
+
throw new ValidationError('File path must be a non-empty string');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!filePath.toLowerCase().endsWith('.tsv')) {
|
|
20
|
+
throw new ValidationError('File must have .tsv extension');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalizedPath = path.normalize(filePath);
|
|
24
|
+
if (normalizedPath.includes('..') ||
|
|
25
|
+
/\\\.\.\\|\/\.\.\//.test(filePath) ||
|
|
26
|
+
filePath.startsWith('..') ||
|
|
27
|
+
filePath.includes('/..')) {
|
|
28
|
+
throw new SecurityError('Directory traversal detected in file path');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return path.resolve(filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class TsvParser {
|
|
35
|
+
/**
|
|
36
|
+
* Конвертирует массив объектов в TSV строку
|
|
37
|
+
* @param {Array} data - Массив объектов
|
|
38
|
+
* @param {Object} options - Опции форматирования
|
|
39
|
+
* @returns {string} TSV строка
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const data = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
|
|
43
|
+
* const tsv = TsvParser.jsonToTsv(data);
|
|
44
|
+
* // Результат: "id\tname\n1\tJohn\n2\tJane"
|
|
45
|
+
*/
|
|
46
|
+
static jsonToTsv(data, options = {}) {
|
|
47
|
+
const defaultOptions = {
|
|
48
|
+
delimiter: '\t',
|
|
49
|
+
includeHeaders: true,
|
|
50
|
+
...options
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return jsonToCsv(data, defaultOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Конвертирует TSV строку в массив объектов
|
|
58
|
+
* @param {string} tsvString - TSV строка
|
|
59
|
+
* @param {Object} options - Опции парсинга
|
|
60
|
+
* @returns {Array} Массив объектов
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const tsv = "id\tname\n1\tJohn\n2\tJane";
|
|
64
|
+
* const data = TsvParser.tsvToJson(tsv);
|
|
65
|
+
* // Результат: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
|
|
66
|
+
*/
|
|
67
|
+
static tsvToJson(tsvString, options = {}) {
|
|
68
|
+
const defaultOptions = {
|
|
69
|
+
delimiter: '\t',
|
|
70
|
+
autoDetect: false,
|
|
71
|
+
hasHeaders: true,
|
|
72
|
+
...options
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return csvToJson(tsvString, defaultOptions);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Автоматически определяет является ли строка TSV
|
|
80
|
+
* @param {string} sample - Образец данных
|
|
81
|
+
* @returns {boolean} True если это TSV
|
|
82
|
+
*/
|
|
83
|
+
static isTsv(sample) {
|
|
84
|
+
if (!sample || typeof sample !== 'string') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lines = sample.split('\n').slice(0, 10);
|
|
89
|
+
let tabCount = 0;
|
|
90
|
+
let commaCount = 0;
|
|
91
|
+
let semicolonCount = 0;
|
|
92
|
+
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.trim() === '') {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Считаем разделители
|
|
99
|
+
tabCount += (line.match(/\t/g) || []).length;
|
|
100
|
+
commaCount += (line.match(/,/g) || []).length;
|
|
101
|
+
semicolonCount += (line.match(/;/g) || []).length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Если табуляций больше чем других разделителей, считаем это TSV
|
|
105
|
+
return tabCount > commaCount && tabCount > semicolonCount;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Создает TransformStream для конвертации JSON в TSV
|
|
110
|
+
* @param {Object} options - Опции конвертации
|
|
111
|
+
* @returns {TransformStream} Transform stream
|
|
112
|
+
*/
|
|
113
|
+
static createJsonToTsvStream(options = {}) {
|
|
114
|
+
const { createJsonToCsvStream } = require('../../stream-json-to-csv');
|
|
115
|
+
|
|
116
|
+
return createJsonToCsvStream({
|
|
117
|
+
delimiter: '\t',
|
|
118
|
+
...options
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Создает TransformStream для конвертации TSV в JSON
|
|
124
|
+
* @param {Object} options - Опции конвертации
|
|
125
|
+
* @returns {TransformStream} Transform stream
|
|
126
|
+
*/
|
|
127
|
+
static createTsvToJsonStream(options = {}) {
|
|
128
|
+
const { createCsvToJsonStream } = require('../../stream-csv-to-json');
|
|
129
|
+
|
|
130
|
+
return createCsvToJsonStream({
|
|
131
|
+
delimiter: '\t',
|
|
132
|
+
autoDetect: false,
|
|
133
|
+
...options
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Читает TSV файл и конвертирует в JSON
|
|
139
|
+
* @param {string} filePath - Путь к TSV файлу
|
|
140
|
+
* @param {Object} options - Опции парсинга
|
|
141
|
+
* @returns {Promise<Array>} Promise с массивом объектов
|
|
142
|
+
*/
|
|
143
|
+
static async readTsvAsJson(filePath, options = {}) {
|
|
144
|
+
const fs = require('fs').promises;
|
|
145
|
+
const safePath = validateTsvFilePath(filePath);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const tsvContent = await fs.readFile(safePath, 'utf8');
|
|
149
|
+
return csvToJson(tsvContent, {
|
|
150
|
+
delimiter: '\t',
|
|
151
|
+
autoDetect: false,
|
|
152
|
+
...options
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof ValidationError || error instanceof SecurityError) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (error.code === 'ENOENT') {
|
|
159
|
+
throw new FileSystemError(`File not found: ${safePath}`, error);
|
|
160
|
+
}
|
|
161
|
+
if (error.code === 'EACCES') {
|
|
162
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
163
|
+
}
|
|
164
|
+
if (error.code === 'EISDIR') {
|
|
165
|
+
throw new FileSystemError(`Path is a directory: ${safePath}`, error);
|
|
166
|
+
}
|
|
167
|
+
throw new FileSystemError(`Failed to read TSV file: ${error.message}`, error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Синхронно читает TSV файл и конвертирует в JSON
|
|
173
|
+
* @param {string} filePath - Путь к TSV файлу
|
|
174
|
+
* @param {Object} options - Опции парсинга
|
|
175
|
+
* @returns {Array} Массив объектов
|
|
176
|
+
*/
|
|
177
|
+
static readTsvAsJsonSync(filePath, options = {}) {
|
|
178
|
+
const fs = require('fs');
|
|
179
|
+
const safePath = validateTsvFilePath(filePath);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const tsvContent = fs.readFileSync(safePath, 'utf8');
|
|
183
|
+
return csvToJson(tsvContent, {
|
|
184
|
+
delimiter: '\t',
|
|
185
|
+
autoDetect: false,
|
|
186
|
+
...options
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof ValidationError || error instanceof SecurityError) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
if (error.code === 'ENOENT') {
|
|
193
|
+
throw new FileSystemError(`File not found: ${safePath}`, error);
|
|
194
|
+
}
|
|
195
|
+
if (error.code === 'EACCES') {
|
|
196
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
197
|
+
}
|
|
198
|
+
if (error.code === 'EISDIR') {
|
|
199
|
+
throw new FileSystemError(`Path is a directory: ${safePath}`, error);
|
|
200
|
+
}
|
|
201
|
+
throw new FileSystemError(`Failed to read TSV file: ${error.message}`, error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Сохраняет массив объектов как TSV файл
|
|
207
|
+
* @param {Array} data - Массив объектов
|
|
208
|
+
* @param {string} filePath - Путь для сохранения
|
|
209
|
+
* @param {Object} options - Опции сохранения
|
|
210
|
+
* @returns {Promise<void>}
|
|
211
|
+
*/
|
|
212
|
+
static async saveAsTsv(data, filePath, options = {}) {
|
|
213
|
+
const fs = require('fs').promises;
|
|
214
|
+
const safePath = validateTsvFilePath(filePath);
|
|
215
|
+
const tsvContent = this.jsonToTsv(data, options);
|
|
216
|
+
const dir = path.dirname(safePath);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await fs.mkdir(dir, { recursive: true });
|
|
220
|
+
await fs.writeFile(safePath, tsvContent, 'utf8');
|
|
221
|
+
return safePath;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.code === 'ENOENT') {
|
|
224
|
+
throw new FileSystemError(`Directory does not exist: ${dir}`, error);
|
|
225
|
+
}
|
|
226
|
+
if (error.code === 'EACCES') {
|
|
227
|
+
throw new FileSystemError(`Permission denied: ${safePath}`, error);
|
|
228
|
+
}
|
|
229
|
+
if (error.code === 'ENOSPC') {
|
|
230
|
+
throw new FileSystemError(`No space left on device: ${safePath}`, error);
|
|
231
|
+
}
|
|
232
|
+
throw new FileSystemError(`Failed to save TSV file: ${error.message}`, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Синхронно сохраняет массив объектов как TSV файл
|
|
238
|
+
* @param {Array} data - Массив объектов
|
|
239
|
+
* @param {string} filePath - Путь для сохранения
|
|
240
|
+
* @param {Object} options - Опции сохранения
|
|
241
|
+
*/
|
|
242
|
+
static saveAsTsvSync(data, filePath, options = {}) {
|
|
243
|
+
const fs = require('fs');
|
|
244
|
+
const safePath = validateTsvFilePath(filePath);
|
|
245
|
+
const tsvContent = this.jsonToTsv(data, options);
|
|
246
|
+
|
|
247
|
+
fs.mkdirSync(path.dirname(safePath), { recursive: true });
|
|
248
|
+
fs.writeFileSync(safePath, tsvContent, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Валидирует TSV строку
|
|
253
|
+
* @param {string} tsvString - TSV строка для валидации
|
|
254
|
+
* @param {Object} options - Опции валидации
|
|
255
|
+
* @returns {Object} Результат валидации
|
|
256
|
+
*/
|
|
257
|
+
static validateTsv(tsvString, options = {}) {
|
|
258
|
+
const { requireConsistentColumns = true } = options;
|
|
259
|
+
|
|
260
|
+
if (!tsvString || typeof tsvString !== 'string') {
|
|
261
|
+
return {
|
|
262
|
+
valid: false,
|
|
263
|
+
error: 'Input must be a non-empty string',
|
|
264
|
+
details: { inputType: typeof tsvString }
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const lines = tsvString.split('\n').filter(line => line.trim() !== '');
|
|
269
|
+
|
|
270
|
+
if (lines.length === 0) {
|
|
271
|
+
return {
|
|
272
|
+
valid: false,
|
|
273
|
+
error: 'No data found in TSV',
|
|
274
|
+
details: { lineCount: 0 }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const columnCounts = [];
|
|
279
|
+
const errors = [];
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < lines.length; i++) {
|
|
282
|
+
const line = lines[i];
|
|
283
|
+
const columns = line.split('\t');
|
|
284
|
+
columnCounts.push(columns.length);
|
|
285
|
+
|
|
286
|
+
// Проверяем наличие пустых полей (если требуется)
|
|
287
|
+
if (options.disallowEmptyFields) {
|
|
288
|
+
const emptyFields = columns.filter(field => field.trim() === '');
|
|
289
|
+
if (emptyFields.length > 0) {
|
|
290
|
+
errors.push({
|
|
291
|
+
line: i + 1,
|
|
292
|
+
error: `Found ${emptyFields.length} empty field(s)`,
|
|
293
|
+
fields: emptyFields.map((_, idx) => idx + 1)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Проверяем консистентность колонки
|
|
300
|
+
if (requireConsistentColumns && columnCounts.length > 1) {
|
|
301
|
+
const firstCount = columnCounts[0];
|
|
302
|
+
const inconsistentLines = [];
|
|
303
|
+
|
|
304
|
+
for (let i = 1; i < columnCounts.length; i++) {
|
|
305
|
+
if (columnCounts[i] !== firstCount) {
|
|
306
|
+
inconsistentLines.push({
|
|
307
|
+
line: i + 1,
|
|
308
|
+
expected: firstCount,
|
|
309
|
+
actual: columnCounts[i]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (inconsistentLines.length > 0) {
|
|
315
|
+
errors.push({
|
|
316
|
+
error: 'Inconsistent column count',
|
|
317
|
+
details: inconsistentLines
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
valid: errors.length === 0,
|
|
324
|
+
stats: {
|
|
325
|
+
totalLines: lines.length,
|
|
326
|
+
totalColumns: columnCounts[0] || 0,
|
|
327
|
+
minColumns: Math.min(...columnCounts),
|
|
328
|
+
maxColumns: Math.max(...columnCounts),
|
|
329
|
+
consistentColumns: new Set(columnCounts).size === 1
|
|
330
|
+
},
|
|
331
|
+
errors: errors.length > 0 ? errors : undefined
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = TsvParser;
|
|
@@ -13,6 +13,7 @@ const NdjsonParser = require('./formats/ndjson-parser');
|
|
|
13
13
|
// Импортируем основные функции
|
|
14
14
|
const coreJsonToCsv = require('../json-to-csv').jsonToCsv;
|
|
15
15
|
const coreCsvToJson = require('../csv-to-json').csvToJson;
|
|
16
|
+
const coreCsvToJsonIterator = require('../csv-to-json').csvToJsonIterator;
|
|
16
17
|
const coreSaveAsCsv = require('../json-to-csv').saveAsCsv;
|
|
17
18
|
const coreReadCsvAsJson = require('../csv-to-json').readCsvAsJson;
|
|
18
19
|
|
|
@@ -155,26 +156,46 @@ class JtcsvWithPlugins {
|
|
|
155
156
|
options,
|
|
156
157
|
(input, opts) => {
|
|
157
158
|
if (this.options.enableFastPath && opts?.useFastPath !== false) {
|
|
158
|
-
|
|
159
|
-
const parsed = this.fastPathEngine.parse(input, opts);
|
|
160
|
-
|
|
161
|
-
// Преобразуем в объекты
|
|
162
|
-
const headers = parsed[0];
|
|
163
|
-
return parsed.slice(1).map(row => {
|
|
164
|
-
const obj = {};
|
|
165
|
-
headers.forEach((header, index) => {
|
|
166
|
-
obj[header] = row[index];
|
|
167
|
-
});
|
|
168
|
-
return obj;
|
|
169
|
-
});
|
|
159
|
+
return coreCsvToJson(input, { ...opts, useFastPath: true });
|
|
170
160
|
}
|
|
171
|
-
|
|
172
|
-
// Используем стандартный парсер
|
|
161
|
+
|
|
173
162
|
return coreCsvToJson(input, opts);
|
|
174
163
|
}
|
|
175
164
|
);
|
|
176
165
|
}
|
|
177
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Convert CSV to JSON rows as async iterator with plugin hooks.
|
|
169
|
+
* @param {string} csv - CSV input
|
|
170
|
+
* @param {Object} options - Conversion options
|
|
171
|
+
* @returns {AsyncGenerator} Async iterator of rows
|
|
172
|
+
*/
|
|
173
|
+
async *csvToJsonIterator(csv, options = {}) {
|
|
174
|
+
if (!this.options.enablePlugins) {
|
|
175
|
+
for await (const row of coreCsvToJsonIterator(csv, options)) {
|
|
176
|
+
yield row;
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const iterator = await this.pluginManager.executeWithPlugins(
|
|
182
|
+
'csvToJson',
|
|
183
|
+
csv,
|
|
184
|
+
options,
|
|
185
|
+
(input, opts) => {
|
|
186
|
+
if (this.options.enableFastPath && opts?.useFastPath !== false) {
|
|
187
|
+
return coreCsvToJsonIterator(input, { ...opts, useFastPath: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return coreCsvToJsonIterator(input, opts);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
for await (const row of iterator) {
|
|
195
|
+
yield row;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
178
199
|
/**
|
|
179
200
|
* Конвертирует JSON в CSV с поддержкой плагинов
|
|
180
201
|
* @param {Array} json - JSON данные
|
|
@@ -347,3 +368,4 @@ module.exports.NdjsonParser = NdjsonParser;
|
|
|
347
368
|
module.exports.create = JtcsvWithPlugins.create;
|
|
348
369
|
|
|
349
370
|
|
|
371
|
+
|