From 3d243665f3c3ae2f78f6c719b8d5500e7fafd80a Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sat, 5 Apr 2025 03:50:55 +0200 Subject: [PATCH 01/10] utils: moving unquote function to utlis (string) --- src/utils/string.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utils/string.ts b/src/utils/string.ts index cbab99c..1bfc3b2 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -46,3 +46,16 @@ export function reverseString(input: string): string { export function containsOnlyDigits(input: string): boolean { return /^\d+(\.\d+)?$/.test(input.trim()); } + +/** + * unquote a string if properly quoted. + * @param value - The string to unquote. + * @param quoteCharacter - The character used for quoting (e.g., '"', "'"). + * @returns The unquoted string if it was quoted, otherwise the original string. + */ +export function unquoteIfQuoted(value: string, quoteCharacter: string): string { + if (value.startsWith(quoteCharacter) && value.endsWith(quoteCharacter)) { + return value.slice(1, -1); // Remove first and last character + } + return value; +} From bafaccc25bfe7225a43f806c4d1f7bda2f6fd374 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sat, 5 Apr 2025 03:51:49 +0200 Subject: [PATCH 02/10] utils: adding delimiter flag for custom delimiters in csv --- src/utils/csv.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/utils/csv.ts b/src/utils/csv.ts index 4830b1f..8fcd52e 100644 --- a/src/utils/csv.ts +++ b/src/utils/csv.ts @@ -8,9 +8,10 @@ export function splitCsv( input: string, deleteComment: boolean, commentCharacter: string, - deleteEmptyLines: boolean + deleteEmptyLines: boolean, + delimiter: string = ',' ): string[][] { - let rows = input.split('\n').map((row) => row.split(',')); + let rows = input.split('\n').map((row) => row.split(delimiter)); // Remove comments if deleteComment is true if (deleteComment && commentCharacter) { @@ -28,9 +29,13 @@ export function splitCsv( /** * get the headers from a CSV string . * @param {string} input - The CSV input string. + * @param {string} csvSeparator - The character used to separate values in the CSV. * @returns {string[]} - The CSV header as a 1D array. */ -export function getCsvHeaders(csvString: string): string[] { - const rows = csvString.split('\n').map((row) => row.split(',')); +export function getCsvHeaders( + csvString: string, + csvSeparator: string = ',' +): string[] { + const rows = csvString.split('\n').map((row) => row.split(csvSeparator)); return rows.length > 0 ? rows[0].map((header) => header.trim()) : []; } From fcb20a24f840a771e1a391409822dd29b67b53a1 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sat, 5 Apr 2025 03:53:15 +0200 Subject: [PATCH 03/10] utils: removing unquote utils function from csv-to-tsv --- src/pages/tools/csv/csv-to-tsv/service.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/tools/csv/csv-to-tsv/service.ts b/src/pages/tools/csv/csv-to-tsv/service.ts index 98ecaf0..a6506f8 100644 --- a/src/pages/tools/csv/csv-to-tsv/service.ts +++ b/src/pages/tools/csv/csv-to-tsv/service.ts @@ -1,9 +1,4 @@ -function unquoteIfQuoted(value: string, quoteCharacter: string): string { - if (value.startsWith(quoteCharacter) && value.endsWith(quoteCharacter)) { - return value.slice(1, -1); // Remove first and last character - } - return value; -} +import { unquoteIfQuoted } from '@utils/string'; export function csvToTsv( input: string, delimiter: string, From a5b8e989ddd30bab5000e2f13a4756b7fde72956 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sat, 5 Apr 2025 03:54:57 +0200 Subject: [PATCH 04/10] feat: csv-to-yaml --- .../csv-to-yaml/csv-to-yaml.service.test.ts | 46 ++++ src/pages/tools/csv/csv-to-yaml/index.tsx | 201 ++++++++++++++++++ src/pages/tools/csv/csv-to-yaml/meta.ts | 15 ++ src/pages/tools/csv/csv-to-yaml/service.ts | 72 +++++++ src/pages/tools/csv/csv-to-yaml/types.ts | 8 + src/pages/tools/csv/index.ts | 4 +- 6 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts create mode 100644 src/pages/tools/csv/csv-to-yaml/index.tsx create mode 100644 src/pages/tools/csv/csv-to-yaml/meta.ts create mode 100644 src/pages/tools/csv/csv-to-yaml/service.ts create mode 100644 src/pages/tools/csv/csv-to-yaml/types.ts diff --git a/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts b/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts new file mode 100644 index 0000000..9205792 --- /dev/null +++ b/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { main } from './service'; +import { InitialValuesType } from './types'; + +// filepath: c:\CODE\omni-tools\src\pages\tools\csv\csv-to-yaml\csv-to-yaml.service.test.ts +describe('main', () => { + const defaultOptions: InitialValuesType = { + csvSeparator: ',', + quoteCharacter: '"', + commentCharacter: '#', + emptyLines: false, + headerRow: true, + spaces: 2 + }; + + it('should return empty string for empty input', () => { + const result = main('', defaultOptions); + expect(result).toBe(''); + }); + + it('should return this if header is set to false', () => { + const options = { ...defaultOptions, headerRow: false }; + const result = main('John,30\nEmma,50', options); + expect(result).toBe(' - John\n - 30\n-\n - Emma\n - 50'); + }); + + it('should return this header is set to true', () => { + const options = { ...defaultOptions }; + const result = main('Name,Age\nJohn,30\nEmma,50', options); + expect(result).toBe(' Name: John\n Age: 30\n-\n Name: Emma\n Age: 50'); + }); + + it('should return this header is set to true and comment flag set', () => { + const options = { ...defaultOptions, commentcharacter: '#' }; + const result = main('Name,Age\nJohn,30\n#Emma,50', options); + expect(result).toBe(' Name: John\n Age: 30'); + }); + + it('should return this header is set to true and spaces is set to 3', () => { + const options = { ...defaultOptions, spaces: 3 }; + const result = main('Name,Age\nJohn,30\nEmma,50', options); + expect(result).toBe( + ' Name: John\n Age: 30\n-\n Name: Emma\n Age: 50' + ); + }); +}); diff --git a/src/pages/tools/csv/csv-to-yaml/index.tsx b/src/pages/tools/csv/csv-to-yaml/index.tsx new file mode 100644 index 0000000..d357fce --- /dev/null +++ b/src/pages/tools/csv/csv-to-yaml/index.tsx @@ -0,0 +1,201 @@ +import { Box } from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolTextInput from '@components/input/ToolTextInput'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { main } from './service'; +import { InitialValuesType } from './types'; +import TextFieldWithDesc from '@components/options/TextFieldWithDesc'; +import CheckboxWithDesc from '@components/options/CheckboxWithDesc'; + +const initialValues: InitialValuesType = { + csvSeparator: ',', + quoteCharacter: '"', + commentCharacter: '#', + emptyLines: true, + headerRow: true, + spaces: 2 +}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Convert Music Playlist CSV to YAML', + description: + 'In this example, we transform a short CSV file containing a music playlist into structured YAML data. The input CSV contains five records with three columns each and the output YAML contains five lists of lists (one list for each CSV record). In YAML, lists start with the "-" symbol and the nested lists are indented with two spaces', + sampleText: `The Beatles,"Yesterday",Pop Rock +Queen,"Bohemian Rhapsody",Rock +Nirvana,"Smells Like Teen Spirit",Grunge +Michael Jackson,"Billie Jean",Pop +Stevie Wonder,"Superstition",Funk`, + sampleResult: ` - The Beatles + - Yesterday + - Pop Rock +- + - Queen + - Bohemian Rhapsody + - Rock +- + - Nirvana + - Smells Like Teen Spirit + - Grunge +- + - Michael Jackson + - Billie Jean + - Pop +- + - Stevie Wonder + - Superstition + - Funk`, + sampleOptions: { + ...initialValues, + headerRow: false + } + }, + { + title: 'Planetary CSV Data', + description: + 'In this example, we are working with CSV data that summarizes key properties of three planets in our solar system. The data consists of three columns with headers "planet", "relative mass" (with "1" being the mass of earth), and "satellites". To preserve the header names in the output YAML data, we enable the "Transform Headers" option, creating a YAML file that contains a list of YAML objects, where each object has three keys: "planet", "relative mass", and "satellites".', + sampleText: `planet,relative mass,satellites +Venus,0.815,0 +Earth,1.000,1 +Mars,0.107,2`, + sampleResult: ` planet: Venus + relative mass: 0.815 + satellites: '0' +- + planet: Earth + relative mass: 1.000 + satellites: '1' +- + planet: Mars + relative mass: 0.107 + satellites: '2'`, + sampleOptions: { + ...initialValues + } + }, + { + title: 'Convert Non-standard CSV to YAML', + description: + 'In this example, we convert a CSV file with non-standard formatting into a regular YAML file. The input data uses a semicolon as a separator for the "product", "quantity", and "price" fields. It also contains empty lines and lines that are commented out. To make the program work with this custom CSV file, we input the semicolon symbol in the CSV delimiter options. To skip comments, we specify "#" as the symbol that starts comments. And to remove empty lines, we activate the option for skipping blank lines (that do not contain any symbols). In the output, we obtain a YAML file that contains a list of three objects, which use CSV headers as keys. Additionally, the objects in the YAML file are indented with four spaces.', + sampleText: `item;quantity;price +milk;2;3.50 + +#eggs;12;2.99 +bread;1;4.25 +#apples;4;1.99 +cheese;1;8.99`, + sampleResult: ` item: milk + quantity: 2 + price: 3.50 +- + item: bread + quantity: 1 + price: 4.25 +- + item: cheese + quantity: 1 + price: 8.99`, + sampleOptions: { + ...initialValues, + csvSeparator: ';' + } + } +]; +export default function CsvToYaml({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (optionsValues: InitialValuesType, input: string) => { + setResult(main(input, optionsValues)); + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'Adjust CSV input', + component: ( + + updateField('csvSeparator', val)} + description={ + 'Enter the character used to delimit columns in the CSV file.' + } + /> + updateField('quoteCharacter', val)} + description={ + 'Enter the quote character used to quote the CSV fields.' + } + /> + updateField('commentCharacter', val)} + description={ + 'Enter the character indicating the start of a comment line. Lines starting with this symbol will be skipped.' + } + /> + + ) + }, + { + title: 'Conversion Options', + component: ( + + updateField('headerRow', value)} + title="Use Headers" + description="Keep the first row as column names." + /> + updateField('emptyLines', value)} + title="Ignore Lines with No Data" + description="Enable to prevent the conversion of empty lines in the input CSV file." + /> + + ) + }, + { + title: 'Adjust YAML indentation', + component: ( + + updateField('spaces', Number(val))} + inputProps={{ min: 0 }} + description={ + 'Set the number of spaces to use for YAML indentation.' + } + /> + + ) + } + ]; + return ( + } + resultComponent={} + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/csv/csv-to-yaml/meta.ts b/src/pages/tools/csv/csv-to-yaml/meta.ts new file mode 100644 index 0000000..9c07601 --- /dev/null +++ b/src/pages/tools/csv/csv-to-yaml/meta.ts @@ -0,0 +1,15 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('csv', { + name: 'Csv to yaml', + path: 'csv-to-yaml', + icon: 'nonicons:yaml-16', + description: + 'Just upload your CSV file in the form below and it will automatically get converted to a YAML file. In the tool options, you can specify the field delimiter character, field quote character, and comment character to adapt the tool to custom CSV formats. Additionally, you can select the output YAML format: one that preserves CSV headers or one that excludes CSV headers.', + shortDescription: 'Quickly convert a CSV file to a YAML file.', + keywords: ['csv', 'to', 'yaml'], + longDescription: + 'This tool transforms CSV (Comma Separated Values) data into the YAML (Yet Another Markup Language) data. CSV is a simple, tabular format that is used to represent matrix-like data types consisting of rows and columns. YAML, on the other hand, is a more advanced format (actually a superset of JSON), which creates more human-readable data for serialization, and it supports lists, dictionaries, and nested objects. This program supports various input CSV formats – the input data can be comma-separated (default), semicolon-separated, pipe-separated, or use another completely different delimiter. You can specify the exact delimiter your data uses in the options. Similarly, in the options, you can specify the quote character that is used to wrap CSV fields (by default a double-quote symbol). You can also skip lines that start with comments by specifying the comment symbols in the options. This allows you to keep your data clean by skipping unnecessary lines. There are two ways to convert CSV to YAML. The first method converts each CSV row into a YAML list. The second method extracts headers from the first CSV row and creates YAML objects with keys based on these headers. You can also customize the output YAML format by specifying the number of spaces for indenting YAML structures. If you need to perform the reverse conversion, that is, transform YAML into CSV, you can use our Convert YAML to CSV tool. Csv-abulous!', + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/csv/csv-to-yaml/service.ts b/src/pages/tools/csv/csv-to-yaml/service.ts new file mode 100644 index 0000000..8557b4d --- /dev/null +++ b/src/pages/tools/csv/csv-to-yaml/service.ts @@ -0,0 +1,72 @@ +import { InitialValuesType } from './types'; +import { getCsvHeaders, splitCsv } from '@utils/csv'; +import { unquoteIfQuoted } from '@utils/string'; + +function toYaml( + input: Record[] | string[][], + indentSpaces: number = 2 +): string { + const indent = ' '.repeat(indentSpaces); + + if ( + Array.isArray(input) && + input.length > 0 && + typeof input[0] === 'object' && + !Array.isArray(input[0]) + ) { + return (input as Record[]) + .map((obj) => { + const lines = Object.entries(obj) + .map(([key, value]) => `${indent}${key}: ${value}`) + .join('\n'); + return `${lines}`; + }) + .join('\n-\n'); + } + + // If input is string[][]. + if (Array.isArray(input) && Array.isArray(input[0])) { + return (input as string[][]) + .map((row) => { + return row.map((cell) => `${indent}- ${cell}`).join('\n'); + }) + .join('\n-\n'); + } + + return 'invalid input'; +} + +export function main(input: string, options: InitialValuesType): string { + if (!input) { + return ''; + } + + const rows = splitCsv( + input, + true, + options.commentCharacter, + options.emptyLines, + options.csvSeparator + ); + + rows.forEach((row) => { + row.forEach((cell, cellIndex) => { + row[cellIndex] = unquoteIfQuoted(cell, options.quoteCharacter); + }); + }); + + if (options.headerRow) { + const headerRow = getCsvHeaders(input, options.csvSeparator); + + const result: Record[] = rows.slice(1).map((row) => { + const entry: Record = {}; + headerRow.forEach((header, headerIndex) => { + entry[header] = row[headerIndex] ?? ''; + }); + return entry; + }); + return toYaml(result, options.spaces); + } + + return toYaml(rows, options.spaces); +} diff --git a/src/pages/tools/csv/csv-to-yaml/types.ts b/src/pages/tools/csv/csv-to-yaml/types.ts new file mode 100644 index 0000000..17ae2e8 --- /dev/null +++ b/src/pages/tools/csv/csv-to-yaml/types.ts @@ -0,0 +1,8 @@ +export type InitialValuesType = { + csvSeparator: string; + quoteCharacter: string; + commentCharacter: string; + emptyLines: boolean; + headerRow: boolean; + spaces: number; +}; diff --git a/src/pages/tools/csv/index.ts b/src/pages/tools/csv/index.ts index aebb587..3c7a65c 100644 --- a/src/pages/tools/csv/index.ts +++ b/src/pages/tools/csv/index.ts @@ -1,3 +1,4 @@ +import { tool as csvToYaml } from './csv-to-yaml/meta'; import { tool as csvToJson } from './csv-to-json/meta'; import { tool as csvToXml } from './csv-to-xml/meta'; import { tool as csvToRowsColumns } from './csv-rows-to-columns/meta'; @@ -9,5 +10,6 @@ export const csvTools = [ csvToXml, csvToRowsColumns, csvToTsv, - swapCsvColumns + swapCsvColumns, + csvToYaml ]; From b96c12bbd2ea66ed214f41f5ece36b1097eea08f Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sat, 5 Apr 2025 13:34:35 +0200 Subject: [PATCH 05/10] fix: remove quote when header flag is set to true --- src/pages/tools/csv/csv-to-yaml/service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/tools/csv/csv-to-yaml/service.ts b/src/pages/tools/csv/csv-to-yaml/service.ts index 8557b4d..67cdefb 100644 --- a/src/pages/tools/csv/csv-to-yaml/service.ts +++ b/src/pages/tools/csv/csv-to-yaml/service.ts @@ -57,6 +57,9 @@ export function main(input: string, options: InitialValuesType): string { if (options.headerRow) { const headerRow = getCsvHeaders(input, options.csvSeparator); + headerRow.forEach((header, headerIndex) => { + headerRow[headerIndex] = unquoteIfQuoted(header, options.quoteCharacter); + }); const result: Record[] = rows.slice(1).map((row) => { const entry: Record = {}; From f6fd1d9a04459c4269156c1a439478cc34e1a2f7 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 6 Apr 2025 01:45:56 +0200 Subject: [PATCH 06/10] fix: edge case in unquote function (when null) --- src/utils/string.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/string.ts b/src/utils/string.ts index 1bfc3b2..a0c2894 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -54,7 +54,11 @@ export function containsOnlyDigits(input: string): boolean { * @returns The unquoted string if it was quoted, otherwise the original string. */ export function unquoteIfQuoted(value: string, quoteCharacter: string): string { - if (value.startsWith(quoteCharacter) && value.endsWith(quoteCharacter)) { + if ( + quoteCharacter && + value.startsWith(quoteCharacter) && + value.endsWith(quoteCharacter) + ) { return value.slice(1, -1); // Remove first and last character } return value; From cdda783e47f3c48ddbf08bdbe499b9bf57da6fed Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 6 Apr 2025 01:47:10 +0200 Subject: [PATCH 07/10] fix: initial (-) added to YAML output --- .../csv/csv-to-yaml/csv-to-yaml.service.test.ts | 14 ++++++++------ src/pages/tools/csv/csv-to-yaml/service.ts | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts b/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts index 9205792..825340e 100644 --- a/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts +++ b/src/pages/tools/csv/csv-to-yaml/csv-to-yaml.service.test.ts @@ -15,32 +15,34 @@ describe('main', () => { it('should return empty string for empty input', () => { const result = main('', defaultOptions); - expect(result).toBe(''); + expect(result).toEqual(''); }); it('should return this if header is set to false', () => { const options = { ...defaultOptions, headerRow: false }; const result = main('John,30\nEmma,50', options); - expect(result).toBe(' - John\n - 30\n-\n - Emma\n - 50'); + expect(result).toEqual('-\n - John\n - 30\n-\n - Emma\n - 50'); }); it('should return this header is set to true', () => { const options = { ...defaultOptions }; const result = main('Name,Age\nJohn,30\nEmma,50', options); - expect(result).toBe(' Name: John\n Age: 30\n-\n Name: Emma\n Age: 50'); + expect(result).toEqual( + '-\n Name: John\n Age: 30\n-\n Name: Emma\n Age: 50' + ); }); it('should return this header is set to true and comment flag set', () => { const options = { ...defaultOptions, commentcharacter: '#' }; const result = main('Name,Age\nJohn,30\n#Emma,50', options); - expect(result).toBe(' Name: John\n Age: 30'); + expect(result).toEqual('-\n Name: John\n Age: 30'); }); it('should return this header is set to true and spaces is set to 3', () => { const options = { ...defaultOptions, spaces: 3 }; const result = main('Name,Age\nJohn,30\nEmma,50', options); - expect(result).toBe( - ' Name: John\n Age: 30\n-\n Name: Emma\n Age: 50' + expect(result).toEqual( + '-\n Name: John\n Age: 30\n-\n Name: Emma\n Age: 50' ); }); }); diff --git a/src/pages/tools/csv/csv-to-yaml/service.ts b/src/pages/tools/csv/csv-to-yaml/service.ts index 67cdefb..587905a 100644 --- a/src/pages/tools/csv/csv-to-yaml/service.ts +++ b/src/pages/tools/csv/csv-to-yaml/service.ts @@ -19,18 +19,19 @@ function toYaml( const lines = Object.entries(obj) .map(([key, value]) => `${indent}${key}: ${value}`) .join('\n'); - return `${lines}`; + return `-\n${lines}`; }) - .join('\n-\n'); + .join('\n'); } // If input is string[][]. if (Array.isArray(input) && Array.isArray(input[0])) { return (input as string[][]) .map((row) => { - return row.map((cell) => `${indent}- ${cell}`).join('\n'); + const inner = row.map((cell) => `${indent}- ${cell}`).join('\n'); + return `-\n${inner}`; }) - .join('\n-\n'); + .join('\n'); } return 'invalid input'; From 0da3189eb3151e66eaedffdbdfe194c128cb09ed Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 6 Apr 2025 01:54:41 +0200 Subject: [PATCH 08/10] fix: examples and giving rigth input's names --- src/pages/tools/csv/csv-to-yaml/index.tsx | 31 +++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/pages/tools/csv/csv-to-yaml/index.tsx b/src/pages/tools/csv/csv-to-yaml/index.tsx index d357fce..c626f8d 100644 --- a/src/pages/tools/csv/csv-to-yaml/index.tsx +++ b/src/pages/tools/csv/csv-to-yaml/index.tsx @@ -30,7 +30,8 @@ Queen,"Bohemian Rhapsody",Rock Nirvana,"Smells Like Teen Spirit",Grunge Michael Jackson,"Billie Jean",Pop Stevie Wonder,"Superstition",Funk`, - sampleResult: ` - The Beatles + sampleResult: `- + - The Beatles - Yesterday - Pop Rock - @@ -62,7 +63,8 @@ Stevie Wonder,"Superstition",Funk`, Venus,0.815,0 Earth,1.000,1 Mars,0.107,2`, - sampleResult: ` planet: Venus + sampleResult: `- + planet: Venus relative mass: 0.815 satellites: '0' - @@ -88,17 +90,18 @@ milk;2;3.50 bread;1;4.25 #apples;4;1.99 cheese;1;8.99`, - sampleResult: ` item: milk - quantity: 2 - price: 3.50 + sampleResult: `- + item: milk + quantity: 2 + price: 3.50 - - item: bread - quantity: 1 - price: 4.25 + item: bread + quantity: 1 + price: 4.25 - - item: cheese - quantity: 1 - price: 8.99`, + item: cheese + quantity: 1 + price: 8.99`, sampleOptions: { ...initialValues, csvSeparator: ';' @@ -188,8 +191,10 @@ export default function CsvToYaml({ } - resultComponent={} + inputComponent={ + + } + resultComponent={} initialValues={initialValues} exampleCards={exampleCards} getGroups={getGroups} From adf72108c6b049cf43858107f0a1415d3eda517a Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Sun, 6 Apr 2025 02:06:15 +0200 Subject: [PATCH 09/10] fix: min indent spaces set to 1 (thrown error when 0) --- src/pages/tools/csv/csv-to-yaml/index.tsx | 2 +- src/pages/tools/csv/csv-to-yaml/service.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/csv/csv-to-yaml/index.tsx b/src/pages/tools/csv/csv-to-yaml/index.tsx index c626f8d..57d8558 100644 --- a/src/pages/tools/csv/csv-to-yaml/index.tsx +++ b/src/pages/tools/csv/csv-to-yaml/index.tsx @@ -178,7 +178,7 @@ export default function CsvToYaml({ value={values.spaces} type="number" onOwnChange={(val) => updateField('spaces', Number(val))} - inputProps={{ min: 0 }} + inputProps={{ min: 1 }} description={ 'Set the number of spaces to use for YAML indentation.' } diff --git a/src/pages/tools/csv/csv-to-yaml/service.ts b/src/pages/tools/csv/csv-to-yaml/service.ts index 587905a..8947516 100644 --- a/src/pages/tools/csv/csv-to-yaml/service.ts +++ b/src/pages/tools/csv/csv-to-yaml/service.ts @@ -6,6 +6,9 @@ function toYaml( input: Record[] | string[][], indentSpaces: number = 2 ): string { + if (indentSpaces == 0) { + throw new Error('Indent spaces must be greater than zero'); + } const indent = ' '.repeat(indentSpaces); if ( From cfc59f3ae032d36c59653986e848cb362bab414f Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Mon, 7 Apr 2025 02:41:20 +0200 Subject: [PATCH 10/10] fix: skip commas between quotes --- src/pages/tools/csv/csv-to-yaml/service.ts | 10 +++- src/utils/csv.ts | 70 ++++++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/pages/tools/csv/csv-to-yaml/service.ts b/src/pages/tools/csv/csv-to-yaml/service.ts index 8947516..7d663fc 100644 --- a/src/pages/tools/csv/csv-to-yaml/service.ts +++ b/src/pages/tools/csv/csv-to-yaml/service.ts @@ -50,7 +50,8 @@ export function main(input: string, options: InitialValuesType): string { true, options.commentCharacter, options.emptyLines, - options.csvSeparator + options.csvSeparator, + options.quoteCharacter ); rows.forEach((row) => { @@ -60,7 +61,12 @@ export function main(input: string, options: InitialValuesType): string { }); if (options.headerRow) { - const headerRow = getCsvHeaders(input, options.csvSeparator); + const headerRow = getCsvHeaders( + input, + options.csvSeparator, + options.quoteCharacter, + options.commentCharacter + ); headerRow.forEach((header, headerIndex) => { headerRow[headerIndex] = unquoteIfQuoted(header, options.quoteCharacter); }); diff --git a/src/utils/csv.ts b/src/utils/csv.ts index 8fcd52e..0aa81df 100644 --- a/src/utils/csv.ts +++ b/src/utils/csv.ts @@ -1,3 +1,41 @@ +/** + * Splits a CSV line into string[], handling quoted string. + * @param {string} input - The CSV input string. + * @param {string} delimiter - The character used to split csvlines. + * @param {string} quoteChar - The character used to quotes csv values. + * @returns {string[][]} - The CSV line as a 1D array. + */ +function splitCsvLine( + line: string, + delimiter: string = ',', + quoteChar: string = '"' +): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const nextChar = line[i + 1]; + + if (char === quoteChar) { + if (inQuotes && nextChar === quoteChar) { + current += quoteChar; + i++; // Skip the escaped quote + } else { + inQuotes = !inQuotes; + } + } else if (char === delimiter && !inQuotes) { + result.push(current.trim()); + current = ''; + } else { + current += char; + } + } + result.push(current.trim()); + return result; +} + /** * Splits a CSV string into rows, skipping any blank lines. * @param {string} input - The CSV input string. @@ -9,9 +47,12 @@ export function splitCsv( deleteComment: boolean, commentCharacter: string, deleteEmptyLines: boolean, - delimiter: string = ',' + delimiter: string = ',', + quoteChar: string = '"' ): string[][] { - let rows = input.split('\n').map((row) => row.split(delimiter)); + let rows = input + .split('\n') + .map((row) => splitCsvLine(row, delimiter, quoteChar)); // Remove comments if deleteComment is true if (deleteComment && commentCharacter) { @@ -30,12 +71,31 @@ export function splitCsv( * get the headers from a CSV string . * @param {string} input - The CSV input string. * @param {string} csvSeparator - The character used to separate values in the CSV. + * @param {string} quoteChar - The character used to quotes csv values. + * @param {string} commentCharacter - The character used to denote comments. * @returns {string[]} - The CSV header as a 1D array. */ export function getCsvHeaders( csvString: string, - csvSeparator: string = ',' + csvSeparator: string = ',', + quoteChar: string = '"', + commentCharacter?: string ): string[] { - const rows = csvString.split('\n').map((row) => row.split(csvSeparator)); - return rows.length > 0 ? rows[0].map((header) => header.trim()) : []; + const lines = csvString.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + + if ( + trimmed === '' || + (commentCharacter && trimmed.startsWith(commentCharacter)) + ) { + continue; // skip empty or commented lines + } + + const headerLine = splitCsvLine(trimmed, csvSeparator, quoteChar); + return headerLine.map((h) => h.replace(/^\uFEFF/, '').trim()); + } + + return []; }