Skip to content

Commit 88d4073

Browse files
committed
feat: tsv to json
1 parent 7302f4a commit 88d4073

File tree

6 files changed

+358
-8
lines changed

6 files changed

+358
-8
lines changed

.idea/workspace.xml

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/pages/tools/json/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { tool as jsonStringify } from './stringify/meta';
44
import { tool as validateJson } from './validateJson/meta';
55
import { tool as jsonToXml } from './json-to-xml/meta';
66
import { tool as escapeJson } from './escape-json/meta';
7+
import { tool as tsvToJson } from './tsv-to-json/meta';
78

89
export const jsonTools = [
910
validateJson,
1011
jsonPrettify,
1112
jsonMinify,
1213
jsonStringify,
1314
jsonToXml,
14-
escapeJson
15+
escapeJson,
16+
tsvToJson
1517
];
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import React, { useState } from 'react';
2+
import ToolContent from '@components/ToolContent';
3+
import ToolTextInput from '@components/input/ToolTextInput';
4+
import ToolTextResult from '@components/result/ToolTextResult';
5+
import { convertTsvToJson } from './service';
6+
import { CardExampleType } from '@components/examples/ToolExamples';
7+
import { GetGroupsType } from '@components/options/ToolOptions';
8+
import { ToolComponentProps } from '@tools/defineTool';
9+
import { Box } from '@mui/material';
10+
import { updateNumberField } from '@utils/string';
11+
import SimpleRadio from '@components/options/SimpleRadio';
12+
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
13+
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
14+
import { InitialValuesType } from './types';
15+
16+
const initialValues: InitialValuesType = {
17+
delimiter: '\t',
18+
quote: '"',
19+
comment: '#',
20+
useHeaders: true,
21+
skipEmptyLines: true,
22+
dynamicTypes: true,
23+
indentationType: 'space',
24+
spacesCount: 2
25+
};
26+
27+
const exampleCards: CardExampleType<InitialValuesType>[] = [
28+
{
29+
title: 'Basic TSV to JSON Array',
30+
description:
31+
'Convert a simple TSV file into a JSON array structure by using spaces as formatting indentation.',
32+
sampleText: `name age city
33+
John 30 New York
34+
Alice 25 London`,
35+
sampleResult: `[
36+
{
37+
"name": "John",
38+
"age": 30,
39+
"city": "New York"
40+
},
41+
{
42+
"name": "Alice",
43+
"age": 25,
44+
"city": "London"
45+
}
46+
]`,
47+
sampleOptions: {
48+
...initialValues,
49+
useHeaders: true,
50+
dynamicTypes: true
51+
}
52+
},
53+
{
54+
title: 'Turn TSV to JSON without Headers',
55+
description: 'Convert a TSV file in minified JSON file.',
56+
sampleText: `Square Triangle Circle
57+
Cube Cone Sphere
58+
#Oval`,
59+
sampleResult: `[["Square","Triangle","Circle"],["Cube","Cone","Sphere"]]`,
60+
sampleOptions: {
61+
...initialValues,
62+
useHeaders: false,
63+
indentationType: 'none'
64+
}
65+
},
66+
{
67+
title: 'Transform TSV to JSON with Headers',
68+
description: 'Convert a TSV file with headers into a JSON file.',
69+
sampleText: `item material quantity
70+
71+
72+
Hat Wool 3
73+
Gloves Leather 5
74+
Candle Wax 4
75+
Vase Glass 2
76+
77+
Sculpture Bronze 1
78+
Table Wood 1
79+
80+
Bookshelf Wood 2`,
81+
sampleResult: `[
82+
{
83+
"item": "Hat",
84+
"material": "Wool",
85+
"quantity": 3
86+
},
87+
{
88+
"item": "Gloves",
89+
"material": "Leather",
90+
"quantity": 5
91+
},
92+
{
93+
"item": "Candle",
94+
"material": "Wax",
95+
"quantity": 4
96+
},
97+
{
98+
"item": "Vase",
99+
"material": "Glass",
100+
"quantity": 2
101+
},
102+
{
103+
"item": "Sculpture",
104+
"material": "Bronze",
105+
"quantity": 1
106+
},
107+
{
108+
"item": "Table",
109+
"material": "Wood",
110+
"quantity": 1
111+
},
112+
{
113+
"item": "Bookshelf",
114+
"material": "Wood",
115+
"quantity": 2
116+
}
117+
]`,
118+
sampleOptions: {
119+
...initialValues
120+
}
121+
}
122+
];
123+
124+
export default function TsvToJson({
125+
title,
126+
longDescription
127+
}: ToolComponentProps) {
128+
const [input, setInput] = useState<string>('');
129+
const [result, setResult] = useState<string>('');
130+
131+
const compute = (values: InitialValuesType, input: string) => {
132+
setResult(convertTsvToJson(input, values));
133+
};
134+
135+
const getGroups: GetGroupsType<InitialValuesType> | null = ({
136+
values,
137+
updateField
138+
}) => [
139+
{
140+
title: 'Input CSV Format',
141+
component: (
142+
<Box>
143+
<TextFieldWithDesc
144+
description="Character used to qutoe tsv values"
145+
onOwnChange={(val) => updateField('quote', val)}
146+
value={values.quote}
147+
/>
148+
<TextFieldWithDesc
149+
description="Symbol use to mark comments in the TSV"
150+
value={values.comment}
151+
onOwnChange={(val) => updateField('comment', val)}
152+
/>
153+
</Box>
154+
)
155+
},
156+
{
157+
title: 'Conversion Options',
158+
component: (
159+
<Box>
160+
<CheckboxWithDesc
161+
checked={values.useHeaders}
162+
onChange={(value) => updateField('useHeaders', value)}
163+
title="Use Headers"
164+
description="First row is treated as column headers"
165+
/>
166+
<CheckboxWithDesc
167+
checked={values.dynamicTypes}
168+
onChange={(value) => updateField('dynamicTypes', value)}
169+
title="Dynamic Types"
170+
description="Convert numbers and booleans to their proper types"
171+
/>
172+
</Box>
173+
)
174+
},
175+
{
176+
title: 'Output Formatting',
177+
component: (
178+
<Box>
179+
<SimpleRadio
180+
onClick={() => updateField('indentationType', 'space')}
181+
checked={values.indentationType === 'space'}
182+
title={'Use Spaces for indentation'}
183+
/>
184+
{values.indentationType === 'space' && (
185+
<TextFieldWithDesc
186+
description="Number of spaces for indentation"
187+
value={values.spacesCount}
188+
onOwnChange={(val) =>
189+
updateNumberField(val, 'spacesCount', updateField)
190+
}
191+
type="number"
192+
/>
193+
)}
194+
<SimpleRadio
195+
onClick={() => updateField('indentationType', 'tab')}
196+
checked={values.indentationType === 'tab'}
197+
title={'Use Tabs for indentation'}
198+
/>
199+
<SimpleRadio
200+
onClick={() => updateField('indentationType', 'none')}
201+
checked={values.indentationType === 'none'}
202+
title={'Minify JSON'}
203+
/>
204+
</Box>
205+
)
206+
}
207+
];
208+
209+
return (
210+
<ToolContent
211+
title={title}
212+
input={input}
213+
setInput={setInput}
214+
initialValues={initialValues}
215+
compute={compute}
216+
exampleCards={exampleCards}
217+
getGroups={getGroups}
218+
inputComponent={
219+
<ToolTextInput title="Input TSV" value={input} onChange={setInput} />
220+
}
221+
resultComponent={
222+
<ToolTextResult title="Output JSON" value={result} extension={'json'} />
223+
}
224+
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
225+
/>
226+
);
227+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineTool } from '@tools/defineTool';
2+
import { lazy } from 'react';
3+
4+
export const tool = defineTool('json', {
5+
name: 'Convert TSV to JSON',
6+
path: 'tsv-to-json',
7+
icon: 'material-symbols:tsv-rounded',
8+
description:
9+
'Convert TSV files to JSON format with customizable options for delimiters, quotes, and output formatting. Support for headers, comments, and dynamic type conversion.',
10+
shortDescription: 'Convert TSV data to JSON format.',
11+
longDescription:
12+
'This tool allows you to convert TSV (Tab-Separated Values) files into JSON format. You can customize the conversion process by specifying delimiters, quote characters, and whether to use headers. It also supports dynamic type conversion for values, handling comments, and skipping empty lines. The output can be formatted with indentation or minified as needed.',
13+
keywords: ['tsv', 'json', 'convert', 'transform', 'parse'],
14+
component: lazy(() => import('./index'))
15+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { InitialValuesType } from './types';
2+
import { beautifyJson } from '../prettify/service';
3+
import { minifyJson } from '../minify/service';
4+
5+
export function convertTsvToJson(
6+
input: string,
7+
options: InitialValuesType
8+
): string {
9+
if (!input) return '';
10+
const lines = input.split('\n');
11+
const result: any[] = [];
12+
let headers: string[] = [];
13+
14+
// Filter out comments and empty lines
15+
const validLines = lines.filter((line) => {
16+
const trimmedLine = line.trim();
17+
return (
18+
trimmedLine &&
19+
(!options.skipEmptyLines ||
20+
!containsOnlyCustomCharAndSpaces(trimmedLine, options.delimiter)) &&
21+
!trimmedLine.startsWith(options.comment)
22+
);
23+
});
24+
25+
if (validLines.length === 0) {
26+
return '[]';
27+
}
28+
29+
// Parse headers if enabled
30+
if (options.useHeaders) {
31+
headers = parseCsvLine(validLines[0], options);
32+
validLines.shift();
33+
}
34+
35+
// Parse data lines
36+
for (const line of validLines) {
37+
const values = parseCsvLine(line, options);
38+
39+
if (options.useHeaders) {
40+
const obj: Record<string, any> = {};
41+
headers.forEach((header, i) => {
42+
obj[header] = parseValue(values[i], options.dynamicTypes);
43+
});
44+
result.push(obj);
45+
} else {
46+
result.push(values.map((v) => parseValue(v, options.dynamicTypes)));
47+
}
48+
}
49+
50+
return options.indentationType === 'none'
51+
? minifyJson(JSON.stringify(result))
52+
: beautifyJson(
53+
JSON.stringify(result),
54+
options.indentationType,
55+
options.spacesCount
56+
);
57+
}
58+
59+
const parseCsvLine = (line: string, options: InitialValuesType): string[] => {
60+
const values: string[] = [];
61+
let currentValue = '';
62+
let inQuotes = false;
63+
64+
for (let i = 0; i < line.length; i++) {
65+
const char = line[i];
66+
67+
if (char === options.quote) {
68+
inQuotes = !inQuotes;
69+
} else if (char === options.delimiter && !inQuotes) {
70+
values.push(currentValue.trim());
71+
currentValue = '';
72+
} else {
73+
currentValue += char;
74+
}
75+
}
76+
77+
values.push(currentValue.trim());
78+
return values;
79+
};
80+
81+
const parseValue = (value: string, dynamicTypes: boolean): any => {
82+
if (!dynamicTypes) return value;
83+
84+
if (value.toLowerCase() === 'true') return true;
85+
if (value.toLowerCase() === 'false') return false;
86+
if (value === 'null') return null;
87+
if (!isNaN(Number(value))) return Number(value);
88+
89+
return value;
90+
};
91+
92+
function containsOnlyCustomCharAndSpaces(str: string, customChar: string) {
93+
const regex = new RegExp(`^[${customChar}\\s]*$`);
94+
return regex.test(str);
95+
}

0 commit comments

Comments
 (0)