feat: tsv to json

This commit is contained in:
Ibrahima G. Coulibaly
2025-06-05 23:15:58 +01:00
parent 7302f4ac93
commit 88d4073f10
6 changed files with 358 additions and 8 deletions

15
.idea/workspace.xml generated
View File

@@ -5,11 +5,12 @@
</component>
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: qr code generation init">
<change afterPath="$PROJECT_DIR$/src/pages/tools/json/tsv-to-json/index.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/json/tsv-to-json/meta.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/json/tsv-to-json/service.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/json/tsv-to-json/types.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/options/ColorSelector.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/ColorSelector.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/generic/qr-code/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/qr-code/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/json/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/json/index.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -227,10 +228,10 @@
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
"Vitest.replaceText function.executor": "Run",
"Vitest.timeBetweenDates.executor": "Run",
"git-widget-placeholder": "qr-code",
"git-widget-placeholder": "main",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/pages/tools/json",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
@@ -264,11 +265,11 @@
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\tools\json" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\@types" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\public\assets" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\components\input" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\.husky" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\lib\ghostscript" />

View File

@@ -4,6 +4,7 @@ import { tool as jsonStringify } from './stringify/meta';
import { tool as validateJson } from './validateJson/meta';
import { tool as jsonToXml } from './json-to-xml/meta';
import { tool as escapeJson } from './escape-json/meta';
import { tool as tsvToJson } from './tsv-to-json/meta';
export const jsonTools = [
validateJson,
@@ -11,5 +12,6 @@ export const jsonTools = [
jsonMinify,
jsonStringify,
jsonToXml,
escapeJson
escapeJson,
tsvToJson
];

View File

@@ -0,0 +1,227 @@
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { convertTsvToJson } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
import { GetGroupsType } from '@components/options/ToolOptions';
import { ToolComponentProps } from '@tools/defineTool';
import { Box } from '@mui/material';
import { updateNumberField } from '@utils/string';
import SimpleRadio from '@components/options/SimpleRadio';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { InitialValuesType } from './types';
const initialValues: InitialValuesType = {
delimiter: '\t',
quote: '"',
comment: '#',
useHeaders: true,
skipEmptyLines: true,
dynamicTypes: true,
indentationType: 'space',
spacesCount: 2
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Basic TSV to JSON Array',
description:
'Convert a simple TSV file into a JSON array structure by using spaces as formatting indentation.',
sampleText: `name age city
John 30 New York
Alice 25 London`,
sampleResult: `[
{
"name": "John",
"age": 30,
"city": "New York"
},
{
"name": "Alice",
"age": 25,
"city": "London"
}
]`,
sampleOptions: {
...initialValues,
useHeaders: true,
dynamicTypes: true
}
},
{
title: 'Turn TSV to JSON without Headers',
description: 'Convert a TSV file in minified JSON file.',
sampleText: `Square Triangle Circle
Cube Cone Sphere
#Oval`,
sampleResult: `[["Square","Triangle","Circle"],["Cube","Cone","Sphere"]]`,
sampleOptions: {
...initialValues,
useHeaders: false,
indentationType: 'none'
}
},
{
title: 'Transform TSV to JSON with Headers',
description: 'Convert a TSV file with headers into a JSON file.',
sampleText: `item material quantity
Hat Wool 3
Gloves Leather 5
Candle Wax 4
Vase Glass 2
Sculpture Bronze 1
Table Wood 1
Bookshelf Wood 2`,
sampleResult: `[
{
"item": "Hat",
"material": "Wool",
"quantity": 3
},
{
"item": "Gloves",
"material": "Leather",
"quantity": 5
},
{
"item": "Candle",
"material": "Wax",
"quantity": 4
},
{
"item": "Vase",
"material": "Glass",
"quantity": 2
},
{
"item": "Sculpture",
"material": "Bronze",
"quantity": 1
},
{
"item": "Table",
"material": "Wood",
"quantity": 1
},
{
"item": "Bookshelf",
"material": "Wood",
"quantity": 2
}
]`,
sampleOptions: {
...initialValues
}
}
];
export default function TsvToJson({
title,
longDescription
}: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (values: InitialValuesType, input: string) => {
setResult(convertTsvToJson(input, values));
};
const getGroups: GetGroupsType<InitialValuesType> | null = ({
values,
updateField
}) => [
{
title: 'Input CSV Format',
component: (
<Box>
<TextFieldWithDesc
description="Character used to qutoe tsv values"
onOwnChange={(val) => updateField('quote', val)}
value={values.quote}
/>
<TextFieldWithDesc
description="Symbol use to mark comments in the TSV"
value={values.comment}
onOwnChange={(val) => updateField('comment', val)}
/>
</Box>
)
},
{
title: 'Conversion Options',
component: (
<Box>
<CheckboxWithDesc
checked={values.useHeaders}
onChange={(value) => updateField('useHeaders', value)}
title="Use Headers"
description="First row is treated as column headers"
/>
<CheckboxWithDesc
checked={values.dynamicTypes}
onChange={(value) => updateField('dynamicTypes', value)}
title="Dynamic Types"
description="Convert numbers and booleans to their proper types"
/>
</Box>
)
},
{
title: 'Output Formatting',
component: (
<Box>
<SimpleRadio
onClick={() => updateField('indentationType', 'space')}
checked={values.indentationType === 'space'}
title={'Use Spaces for indentation'}
/>
{values.indentationType === 'space' && (
<TextFieldWithDesc
description="Number of spaces for indentation"
value={values.spacesCount}
onOwnChange={(val) =>
updateNumberField(val, 'spacesCount', updateField)
}
type="number"
/>
)}
<SimpleRadio
onClick={() => updateField('indentationType', 'tab')}
checked={values.indentationType === 'tab'}
title={'Use Tabs for indentation'}
/>
<SimpleRadio
onClick={() => updateField('indentationType', 'none')}
checked={values.indentationType === 'none'}
title={'Minify JSON'}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
input={input}
setInput={setInput}
initialValues={initialValues}
compute={compute}
exampleCards={exampleCards}
getGroups={getGroups}
inputComponent={
<ToolTextInput title="Input TSV" value={input} onChange={setInput} />
}
resultComponent={
<ToolTextResult title="Output JSON" value={result} extension={'json'} />
}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}

View File

@@ -0,0 +1,15 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('json', {
name: 'Convert TSV to JSON',
path: 'tsv-to-json',
icon: 'material-symbols:tsv-rounded',
description:
'Convert TSV files to JSON format with customizable options for delimiters, quotes, and output formatting. Support for headers, comments, and dynamic type conversion.',
shortDescription: 'Convert TSV data to JSON format.',
longDescription:
'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.',
keywords: ['tsv', 'json', 'convert', 'transform', 'parse'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,95 @@
import { InitialValuesType } from './types';
import { beautifyJson } from '../prettify/service';
import { minifyJson } from '../minify/service';
export function convertTsvToJson(
input: string,
options: InitialValuesType
): string {
if (!input) return '';
const lines = input.split('\n');
const result: any[] = [];
let headers: string[] = [];
// Filter out comments and empty lines
const validLines = lines.filter((line) => {
const trimmedLine = line.trim();
return (
trimmedLine &&
(!options.skipEmptyLines ||
!containsOnlyCustomCharAndSpaces(trimmedLine, options.delimiter)) &&
!trimmedLine.startsWith(options.comment)
);
});
if (validLines.length === 0) {
return '[]';
}
// Parse headers if enabled
if (options.useHeaders) {
headers = parseCsvLine(validLines[0], options);
validLines.shift();
}
// Parse data lines
for (const line of validLines) {
const values = parseCsvLine(line, options);
if (options.useHeaders) {
const obj: Record<string, any> = {};
headers.forEach((header, i) => {
obj[header] = parseValue(values[i], options.dynamicTypes);
});
result.push(obj);
} else {
result.push(values.map((v) => parseValue(v, options.dynamicTypes)));
}
}
return options.indentationType === 'none'
? minifyJson(JSON.stringify(result))
: beautifyJson(
JSON.stringify(result),
options.indentationType,
options.spacesCount
);
}
const parseCsvLine = (line: string, options: InitialValuesType): string[] => {
const values: string[] = [];
let currentValue = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === options.quote) {
inQuotes = !inQuotes;
} else if (char === options.delimiter && !inQuotes) {
values.push(currentValue.trim());
currentValue = '';
} else {
currentValue += char;
}
}
values.push(currentValue.trim());
return values;
};
const parseValue = (value: string, dynamicTypes: boolean): any => {
if (!dynamicTypes) return value;
if (value.toLowerCase() === 'true') return true;
if (value.toLowerCase() === 'false') return false;
if (value === 'null') return null;
if (!isNaN(Number(value))) return Number(value);
return value;
};
function containsOnlyCustomCharAndSpaces(str: string, customChar: string) {
const regex = new RegExp(`^[${customChar}\\s]*$`);
return regex.test(str);
}

View File

@@ -0,0 +1,10 @@
export type InitialValuesType = {
delimiter: string;
quote: string;
comment: string;
useHeaders: boolean;
skipEmptyLines: boolean;
dynamicTypes: boolean;
indentationType: 'tab' | 'space' | 'none';
spacesCount: number;
};