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.
Files changed (40) hide show
  1. package/README.md +63 -17
  2. package/bin/jtcsv.js +1013 -117
  3. package/csv-to-json.js +385 -311
  4. package/examples/simple-usage.js +2 -3
  5. package/index.d.ts +288 -5
  6. package/index.js +23 -0
  7. package/json-to-csv.js +130 -89
  8. package/package.json +47 -19
  9. package/plugins/README.md +146 -2
  10. package/plugins/hono/README.md +25 -0
  11. package/plugins/hono/index.d.ts +12 -0
  12. package/plugins/hono/index.js +36 -0
  13. package/plugins/hono/package.json +35 -0
  14. package/plugins/nestjs/README.md +33 -0
  15. package/plugins/nestjs/index.d.ts +25 -0
  16. package/plugins/nestjs/index.js +77 -0
  17. package/plugins/nestjs/package.json +37 -0
  18. package/plugins/nuxt/README.md +25 -0
  19. package/plugins/nuxt/index.js +21 -0
  20. package/plugins/nuxt/package.json +35 -0
  21. package/plugins/nuxt/runtime/composables/useJtcsv.js +6 -0
  22. package/plugins/nuxt/runtime/plugin.js +6 -0
  23. package/plugins/remix/README.md +26 -0
  24. package/plugins/remix/index.d.ts +16 -0
  25. package/plugins/remix/index.js +62 -0
  26. package/plugins/remix/package.json +35 -0
  27. package/plugins/sveltekit/README.md +28 -0
  28. package/plugins/sveltekit/index.d.ts +17 -0
  29. package/plugins/sveltekit/index.js +54 -0
  30. package/plugins/sveltekit/package.json +33 -0
  31. package/plugins/trpc/README.md +22 -0
  32. package/plugins/trpc/index.d.ts +7 -0
  33. package/plugins/trpc/index.js +32 -0
  34. package/plugins/trpc/package.json +34 -0
  35. package/src/core/delimiter-cache.js +186 -0
  36. package/src/core/transform-hooks.js +350 -0
  37. package/src/engines/fast-path-engine.js +829 -340
  38. package/src/formats/tsv-parser.js +336 -0
  39. package/src/index-with-plugins.js +36 -14
  40. 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
- // Используем fast path engine
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
+
package/cli-tui.js DELETED
@@ -1,5 +0,0 @@
1
- // TUI Interface for jtcsv
2
-
3
-
4
-
5
-