From 8559369475d021691d8d20f61d6186f5acf516a8 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Thu, 5 Jun 2025 19:52:13 +0200 Subject: [PATCH] feat tsv to json --- src/pages/tools/json/index.ts | 4 +- src/pages/tools/json/tsv-to-json/index.tsx | 227 ++++++++++++++++++++ src/pages/tools/json/tsv-to-json/meta.ts | 15 ++ src/pages/tools/json/tsv-to-json/service.ts | 95 ++++++++ src/pages/tools/json/tsv-to-json/types.ts | 10 + 5 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/json/tsv-to-json/index.tsx create mode 100644 src/pages/tools/json/tsv-to-json/meta.ts create mode 100644 src/pages/tools/json/tsv-to-json/service.ts create mode 100644 src/pages/tools/json/tsv-to-json/types.ts diff --git a/src/pages/tools/json/index.ts b/src/pages/tools/json/index.ts index 864e303..04a7940 100644 --- a/src/pages/tools/json/index.ts +++ b/src/pages/tools/json/index.ts @@ -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 ]; diff --git a/src/pages/tools/json/tsv-to-json/index.tsx b/src/pages/tools/json/tsv-to-json/index.tsx new file mode 100644 index 0000000..9479788 --- /dev/null +++ b/src/pages/tools/json/tsv-to-json/index.tsx @@ -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[] = [ + { + 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(''); + const [result, setResult] = useState(''); + + const compute = (values: InitialValuesType, input: string) => { + setResult(convertTsvToJson(input, values)); + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'Input CSV Format', + component: ( + + updateField('quote', val)} + value={values.quote} + /> + updateField('comment', val)} + /> + + ) + }, + { + title: 'Conversion Options', + component: ( + + updateField('useHeaders', value)} + title="Use Headers" + description="First row is treated as column headers" + /> + updateField('dynamicTypes', value)} + title="Dynamic Types" + description="Convert numbers and booleans to their proper types" + /> + + ) + }, + { + title: 'Output Formatting', + component: ( + + updateField('indentationType', 'space')} + checked={values.indentationType === 'space'} + title={'Use Spaces for indentation'} + /> + {values.indentationType === 'space' && ( + + updateNumberField(val, 'spacesCount', updateField) + } + type="number" + /> + )} + updateField('indentationType', 'tab')} + checked={values.indentationType === 'tab'} + title={'Use Tabs for indentation'} + /> + updateField('indentationType', 'none')} + checked={values.indentationType === 'none'} + title={'Minify JSON'} + /> + + ) + } + ]; + + return ( + + } + resultComponent={ + + } + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/json/tsv-to-json/meta.ts b/src/pages/tools/json/tsv-to-json/meta.ts new file mode 100644 index 0000000..fae6c82 --- /dev/null +++ b/src/pages/tools/json/tsv-to-json/meta.ts @@ -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: 'lets-icons:json-light', + 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')) +}); diff --git a/src/pages/tools/json/tsv-to-json/service.ts b/src/pages/tools/json/tsv-to-json/service.ts new file mode 100644 index 0000000..f33b243 --- /dev/null +++ b/src/pages/tools/json/tsv-to-json/service.ts @@ -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 = {}; + 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); +} diff --git a/src/pages/tools/json/tsv-to-json/types.ts b/src/pages/tools/json/tsv-to-json/types.ts new file mode 100644 index 0000000..2733eb0 --- /dev/null +++ b/src/pages/tools/json/tsv-to-json/types.ts @@ -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; +};