refactor: tools folder inside pages

This commit is contained in:
Ibrahima G. Coulibaly
2025-02-23 01:38:42 +01:00
parent 62f084eb45
commit 64936ab11f
117 changed files with 447 additions and 194 deletions

View File

@@ -0,0 +1,3 @@
import { pngTools } from './png';
export const imageTools = [...pngTools];

View File

@@ -0,0 +1,43 @@
import { expect, test } from '@playwright/test';
import { Buffer } from 'buffer';
import path from 'path';
import Jimp from 'jimp';
import { convertHexToRGBA } from '../../../../../utils/color';
test.describe('Change colors in png', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/png/change-colors-in-png');
});
test('should change pixel color', async ({ page }) => {
// Upload image
const fileInput = page.locator('input[type="file"]');
const imagePath = path.join(__dirname, 'test.png');
await fileInput?.setInputFiles(imagePath);
await page.getByTestId('from-color-input').fill('#FF0000');
const toColor = '#0000FF';
await page.getByTestId('to-color-input').fill(toColor);
// Click on download
const downloadPromise = page.waitForEvent('download');
await page.getByText('Save as').click();
// Intercept and read downloaded PNG
const download = await downloadPromise;
const downloadStream = await download.createReadStream();
const chunks = [];
for await (const chunk of downloadStream) {
chunks.push(chunk);
}
const fileContent = Buffer.concat(chunks);
expect(fileContent.length).toBeGreaterThan(0);
// Check that the first pixel is transparent
const image = await Jimp.read(fileContent);
const color = image.getPixelColor(0, 0);
expect(color).toBe(convertHexToRGBA(toColor));
});
});

View File

@@ -0,0 +1,140 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '@components/result/ToolFileResult';
import ToolOptions from '@components/options/ToolOptions';
import ColorSelector from '@components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color';
const initialValues = {
fromColor: 'white',
toColor: 'black',
similarity: '10'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const compute = (optionsValues: typeof initialValues, input: any) => {
if (!input) return;
const { fromColor, toColor, similarity } = optionsValues;
let fromRgb: [number, number, number];
let toRgb: [number, number, number];
try {
//@ts-ignore
fromRgb = Color(fromColor).rgb().array();
//@ts-ignore
toRgb = Color(toColor).rgb().array();
} catch (err) {
return;
}
const processImage = async (
file: File,
fromColor: [number, number, number],
toColor: [number, number, number],
similarity: number
) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data: Uint8ClampedArray = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const currentColor: [number, number, number] = [
data[i],
data[i + 1],
data[i + 2]
];
if (areColorsSimilar(currentColor, fromColor, similarity)) {
data[i] = toColor[0]; // Red
data[i + 1] = toColor[1]; // Green
data[i + 2] = toColor[2]; // Blue
}
}
ctx.putImageData(imageData, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, {
type: 'image/png'
});
setResult(newFile);
}
}, 'image/png');
};
processImage(input, fromRgb, toRgb, Number(similarity));
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Output PNG with new colors'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'From color and to color',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onColorChange={(val) => updateField('fromColor', val)}
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'from-color-input' }}
/>
<ColorSelector
value={values.toColor}
onColorChange={(val) => updateField('toColor', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'to-color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Change colors in png',
path: 'change-colors-in-png',
image,
description:
"World's simplest online Portable Network Graphics (PNG) color changer. Just import your PNG image in the editor on the left, select which colors to change, and you'll instantly get a new PNG with the new colors on the right. Free, quick, and very powerful. Import a PNG replace its colors.",
shortDescription: 'Quickly swap colors in a PNG image',
keywords: ['change', 'colors', 'in', 'png'],
component: lazy(() => import('./index'))
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,72 @@
import { expect, test } from '@playwright/test';
import { Buffer } from 'buffer';
import path from 'path';
import Jimp from 'jimp';
test.describe('Convert JPG to PNG tool', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/png/convert-jgp-to-png');
});
test('should convert jpg to png', async ({ page }) => {
// Upload image
const fileInput = page.locator('input[type="file"]');
const imagePath = path.join(__dirname, 'test.jpg');
await fileInput?.setInputFiles(imagePath);
// Click on download
const downloadPromise = page.waitForEvent('download');
await page.getByText('Save as').click();
// Intercept and read downloaded PNG
const download = await downloadPromise;
const downloadStream = await download.createReadStream();
const chunks = [];
for await (const chunk of downloadStream) {
chunks.push(chunk);
}
const fileContent = Buffer.concat(chunks);
expect(fileContent.length).toBeGreaterThan(0);
// Check that the first pixel is 0x808080ff
const image = await Jimp.read(fileContent);
const color = image.getPixelColor(0, 0);
expect(color).toBe(0x808080ff);
});
test('should apply transparency before converting jpg to png', async ({
page
}) => {
// Upload image
const fileInput = page.locator('input[type="file"]');
const imagePath = path.join(__dirname, 'test.jpg');
await fileInput?.setInputFiles(imagePath);
// Enable transparency on color 0x808080
await page.getByLabel('Enable PNG Transparency').check();
await page.getByTestId('color-input').fill('#808080');
// Click on download
const downloadPromise = page.waitForEvent('download');
await page.getByText('Save as').click();
// Intercept and read downloaded PNG
const download = await downloadPromise;
const downloadStream = await download.createReadStream();
const chunks = [];
for await (const chunk of downloadStream) {
chunks.push(chunk);
}
const fileContent = Buffer.concat(chunks);
expect(fileContent.length).toBeGreaterThan(0);
// Check that the first pixel is transparent
const image = await Jimp.read(fileContent);
const color = image.getPixelColor(0, 0);
expect(color).toBe(0);
});
});

View File

@@ -0,0 +1,154 @@
import { Box } from '@mui/material';
import ToolInputAndResult from 'components/ToolInputAndResult';
import ToolFileInput from 'components/input/ToolFileInput';
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
import ColorSelector from 'components/options/ColorSelector';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolOptions from 'components/options/ToolOptions';
import ToolFileResult from 'components/result/ToolFileResult';
import Color from 'color';
import React, { useState } from 'react';
import * as Yup from 'yup';
import { areColorsSimilar } from 'utils/color';
const initialValues = {
enableTransparency: false,
color: 'white',
similarity: '10'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ConvertJgpToPng() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const compute = async (
optionsValues: typeof initialValues,
input: any
): Promise<void> => {
if (!input) return;
const processImage = async (
file: File,
transparencyTransform?: {
color: [number, number, number];
similarity: number;
}
) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
if (transparencyTransform) {
const { color, similarity } = transparencyTransform;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data: Uint8ClampedArray = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const currentColor: [number, number, number] = [
data[i],
data[i + 1],
data[i + 2]
];
if (areColorsSimilar(currentColor, color, similarity)) {
data[i + 3] = 0;
}
}
ctx.putImageData(imageData, 0, 0);
}
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, {
type: 'image/png'
});
setResult(newFile);
}
}, 'image/png');
};
if (optionsValues.enableTransparency) {
let rgb: [number, number, number];
try {
//@ts-ignore
rgb = Color(optionsValues.color).rgb().array();
} catch (err) {
return;
}
processImage(input, {
color: rgb,
similarity: Number(optionsValues.similarity)
});
} else {
processImage(input);
}
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/jpeg']}
title={'Input JPG'}
/>
}
result={
<ToolFileResult
title={'Output PNG'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'PNG Transparency Color',
component: (
<Box>
<CheckboxWithDesc
key="enableTransparency"
title="Enable PNG Transparency"
checked={!!values.enableTransparency}
onChange={(value) => updateField('enableTransparency', value)}
description="Make the color below transparent."
/>
<ColorSelector
value={values.color}
onColorChange={(val) => updateField('color', val)}
description={'With this color (to color)'}
inputProps={{ 'data-testid': 'color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Convert JPG to PNG',
path: 'convert-jgp-to-png',
image,
description:
'Quickly convert your JPG images to PNG. Just import your PNG image in the editor on the left',
shortDescription: 'Quickly convert your JPG images to PNG',
keywords: ['convert', 'jgp', 'png'],
component: lazy(() => import('./index'))
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,40 @@
import { expect, test } from '@playwright/test';
import { Buffer } from 'buffer';
import path from 'path';
import Jimp from 'jimp';
test.describe('Create transparent PNG', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/png/create-transparent');
});
test('should make png color transparent', async ({ page }) => {
// Upload image
const fileInput = page.locator('input[type="file"]');
const imagePath = path.join(__dirname, 'test.png');
await fileInput?.setInputFiles(imagePath);
await page.getByTestId('color-input').fill('#FF0000');
// Click on download
const downloadPromise = page.waitForEvent('download');
await page.getByText('Save as').click();
// Intercept and read downloaded PNG
const download = await downloadPromise;
const downloadStream = await download.createReadStream();
const chunks = [];
for await (const chunk of downloadStream) {
chunks.push(chunk);
}
const fileContent = Buffer.concat(chunks);
expect(fileContent.length).toBeGreaterThan(0);
// Check that the first pixel is transparent
const image = await Jimp.read(fileContent);
const color = image.getPixelColor(0, 0);
expect(color).toBe(0);
});
});

View File

@@ -0,0 +1,127 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '@components/result/ToolFileResult';
import ToolOptions from '@components/options/ToolOptions';
import ColorSelector from '@components/options/ColorSelector';
import Color from 'color';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
import { areColorsSimilar } from 'utils/color';
const initialValues = {
fromColor: 'white',
similarity: '10'
};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ChangeColorsInPng() {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const compute = (optionsValues: typeof initialValues, input: any) => {
if (!input) return;
const { fromColor, similarity } = optionsValues;
let fromRgb: [number, number, number];
try {
//@ts-ignore
fromRgb = Color(fromColor).rgb().array();
} catch (err) {
return;
}
const processImage = async (
file: File,
fromColor: [number, number, number],
similarity: number
) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx == null) return;
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data: Uint8ClampedArray = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const currentColor: [number, number, number] = [
data[i],
data[i + 1],
data[i + 2]
];
if (areColorsSimilar(currentColor, fromColor, similarity)) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
}
}
ctx.putImageData(imageData, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, { type: 'image/png' });
setResult(newFile);
}
}, 'image/png');
};
processImage(input, fromRgb, Number(similarity));
};
return (
<Box>
<ToolInputAndResult
input={
<ToolFileInput
value={input}
onChange={setInput}
accept={['image/png']}
title={'Input PNG'}
/>
}
result={
<ToolFileResult
title={'Transparent PNG'}
value={result}
extension={'png'}
/>
}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'From color and similarity',
component: (
<Box>
<ColorSelector
value={values.fromColor}
onColorChange={(val) => updateField('fromColor', val)}
description={'Replace this color (from color)'}
inputProps={{ 'data-testid': 'color-input' }}
/>
<TextFieldWithDesc
value={values.similarity}
onOwnChange={(val) => updateField('similarity', val)}
description={
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/image.png';
export const tool = defineTool('png', {
name: 'Create transparent PNG',
path: 'create-transparent',
image,
shortDescription: 'Quickly make a PNG image transparent',
description:
"World's simplest online Portable Network Graphics transparency maker. Just import your PNG image in the editor on the left and you will instantly get a transparent PNG on the right. Free, quick, and very powerful. Import a PNG get a transparent PNG.",
keywords: ['create', 'transparent'],
component: lazy(() => import('./index'))
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,9 @@
import { tool as convertJgpToPng } from './convert-jgp-to-png/meta';
import { tool as pngCreateTransparent } from './create-transparent/meta';
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
export const pngTools = [
changeColorsInPng,
pngCreateTransparent,
convertJgpToPng
];

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { duplicateList } from './service';
describe('duplicateList function', () => {
it('should duplicate elements correctly with symbol split', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, false, 2);
expect(result).toBe('Hello World Hello World');
});
it('should duplicate elements correctly with regex split', () => {
const input = 'Hello||World';
const result = duplicateList('regex', '\\|\\|', ' ', input, true, false, 2);
expect(result).toBe('Hello World Hello World');
});
it('should handle fractional duplication', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, false, 1.5);
expect(result).toBe('Hello World Hello');
});
it('should handle reverse option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, true, true, 2);
expect(result).toBe('Hello World World Hello');
});
it('should handle concatenate option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe('Hello Hello World World');
});
it('should handle interweaving option correctly', () => {
const input = 'Hello World';
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
expect(result).toBe('Hello Hello World World');
});
it('should throw an error for negative copies', () => {
expect(() =>
duplicateList('symbol', ' ', ' ', 'Hello World', true, false, -1)
).toThrow('Number of copies cannot be negative');
});
it('should handle interweaving option correctly 2', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 2.1);
expect(result).toBe("je, king, m'appelle, m'appelle, king, je");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, false, true, 1);
expect(result).toBe("je, m'appelle, king");
});
it('should handle interweaving option correctly 3', () => {
const input = "je m'appelle king";
const result = duplicateList('symbol', ' ', ', ', input, true, true, 2.7);
expect(result).toBe(
"je, m'appelle, king, king, m'appelle, je, king, m'appelle"
);
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Duplicate() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Duplicate',
path: 'duplicate',
// image,
description: '',
shortDescription: '',
keywords: ['duplicate'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,81 @@
export type SplitOperatorType = 'symbol' | 'regex';
function interweave(array1: string[], array2: string[]) {
const result: string[] = [];
const maxLength = Math.max(array1.length, array2.length);
for (let i = 0; i < maxLength; i++) {
if (i < array1.length) result.push(array1[i]);
if (i < array2.length) result.push(array2[i]);
}
return result;
}
function duplicate(
input: string[],
concatenate: boolean,
reverse: boolean,
copy?: number
) {
if (copy) {
if (copy > 0) {
let result: string[] = [];
let toAdd: string[] = [];
let WholePart: string[] = [];
let fractionalPart: string[] = [];
const whole = Math.floor(copy);
const fractional = copy - whole;
if (!reverse) {
WholePart = concatenate
? Array(whole).fill(input).flat()
: Array(whole - 1)
.fill(input)
.flat();
fractionalPart = input.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate
? WholePart.concat(fractionalPart)
: interweave(input, toAdd);
} else {
WholePart = Array(whole - 1)
.fill(input)
.flat()
.reverse();
fractionalPart = input
.slice()
.reverse()
.slice(0, Math.floor(input.length * fractional));
toAdd = WholePart.concat(fractionalPart);
result = concatenate ? input.concat(toAdd) : interweave(input, toAdd);
}
return result;
}
throw new Error('Number of copies cannot be negative');
}
throw new Error('Number of copies must be a valid number');
}
export function duplicateList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
joinSeparator: string,
input: string,
concatenate: boolean,
reverse: boolean,
copy?: number
): string {
let array: string[];
let result: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input
.split(new RegExp(splitSeparator))
.filter((item) => item !== '');
break;
}
result = duplicate(array, concatenate, reverse, copy);
return result.join(joinSeparator);
}

View File

@@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest';
import { TopItemsList } from './service';
describe('TopItemsList function', () => {
it('should handle sorting alphabetically ignoring case', () => {
const input = 'Apple,banana,apple,Orange,Banana,apple';
const result = TopItemsList(
'symbol',
'alphabetic',
'count',
',',
input,
false,
true,
false
);
expect(result).toEqual('apple: 3\n' + 'banana: 2\n' + 'orange: 1');
});
it('should handle sorting by count and not ignoring case', () => {
const input = 'apple,banana,apple,orange,banana,apple,Banana';
const result = TopItemsList(
'symbol',
'count',
'count',
',',
input,
false,
false,
false
);
expect(result).toEqual(
'apple: 3\n' + 'banana: 2\n' + 'orange: 1\n' + 'Banana: 1'
);
});
it('should handle regex split operator', () => {
const input = 'apple123banana456apple789orange012banana345apple678';
const result = TopItemsList(
'regex',
'count',
'count',
'\\d+',
input,
false,
false,
false
);
expect(result).toEqual('apple: 3\n' + 'banana: 2\n' + 'orange: 1');
});
it('should handle percentage display format', () => {
const input = 'apple,banana,apple,orange,banana,apple';
const result = TopItemsList(
'symbol',
'count',
'percentage',
',',
input,
false,
false,
false
);
expect(result).toEqual(
'apple: 3 (50.00%)\n' + 'banana: 2 (33.33%)\n' + 'orange: 1 (16.67%)'
);
});
it('should handle total display format', () => {
const input = 'apple,banana,apple,orange,banana,apple';
const result = TopItemsList(
'symbol',
'count',
'total',
',',
input,
false,
false,
false
);
expect(result).toEqual(
'apple: 3 (3 / 6)\n' + 'banana: 2 (2 / 6)\n' + 'orange: 1 (1 / 6)'
);
});
it('should handle trimming and ignoring empty items', () => {
const input = ' apple , banana , apple , orange , banana , apple ';
const result = TopItemsList(
'symbol',
'count',
'count',
',',
input,
true,
false,
true
);
expect(result).toEqual('apple: 3\n' + 'banana: 2\n' + 'orange: 1');
});
});

View File

@@ -0,0 +1,167 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import {
DisplayFormat,
SortingMethod,
SplitOperatorType,
TopItemsList
} from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import SelectWithDesc from '@components/options/SelectWithDesc';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
sortingMethod: 'alphabetic' as SortingMethod,
displayFormat: 'count' as DisplayFormat,
splitSeparator: ',',
deleteEmptyItems: false,
ignoreItemCase: false,
trimItems: false
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
export default function FindMostPopular() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
splitSeparator,
displayFormat,
sortingMethod,
deleteEmptyItems,
ignoreItemCase,
trimItems
} = optionsValues;
setResult(
TopItemsList(
splitSeparatorType,
sortingMethod,
displayFormat,
splitSeparator,
input,
deleteEmptyItems,
ignoreItemCase,
trimItems
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Most popular items'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'How to Extract List Items?',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitSeparatorType', type)}
title={title}
description={description}
checked={values.splitSeparatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Item comparison',
component: (
<Box>
<CheckboxWithDesc
title={'Remove empty items'}
description={'Ignore empty items from comparison.'}
checked={values.deleteEmptyItems}
onChange={(value) => updateField('deleteEmptyItems', value)}
/>
<CheckboxWithDesc
title={'Trim top list items'}
description={
'Remove leading and trailing spaces before comparing items'
}
checked={values.trimItems}
onChange={(value) => updateField('trimItems', value)}
/>
<CheckboxWithDesc
title={'Ignore Item Case'}
description={'Compare all list items in lowercase.'}
checked={values.ignoreItemCase}
onChange={(value) => updateField('ignoreItemCase', value)}
/>
</Box>
)
},
{
title: 'Top item output format',
component: (
<Box>
<SelectWithDesc
selected={values.displayFormat}
options={[
{ label: 'Show item percentage', value: 'percentage' },
{ label: 'Show item count', value: 'count' },
{ label: 'Show item total', value: 'total' }
]}
onChange={(value) => updateField('displayFormat', value)}
description={'How to display the most popular list items?'}
/>
<SelectWithDesc
selected={values.sortingMethod}
options={[
{ label: 'Sort Alphabetically', value: 'alphabetic' },
{ label: 'Sort by count', value: 'count' }
]}
onChange={(value) => updateField('sortingMethod', value)}
description={'Select a sorting method.'}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Find most popular',
path: 'find-most-popular',
// image,
description: '',
shortDescription: '',
keywords: ['find', 'most', 'popular'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,114 @@
export type SplitOperatorType = 'symbol' | 'regex';
export type DisplayFormat = 'count' | 'percentage' | 'total';
export type SortingMethod = 'count' | 'alphabetic';
// Function that takes the array as arg and returns a dict of element occurrences and handle the ignoreItemCase
function dictMaker(
array: string[],
ignoreItemCase: boolean
): { [key: string]: number } {
const dict: { [key: string]: number } = {};
for (const item of array) {
const key = ignoreItemCase ? item.toLowerCase() : item;
dict[key] = (dict[key] || 0) + 1;
}
return dict;
}
// Function that sorts the dict created with dictMaker based on the chosen sorting method
function dictSorter(
dict: { [key: string]: number },
sortingMethod: SortingMethod
): { [key: string]: number } {
let sortedArray: [string, number][];
switch (sortingMethod) {
case 'count':
sortedArray = Object.entries(dict).sort(
([, countA], [, countB]) => countB - countA
);
break;
case 'alphabetic':
sortedArray = Object.entries(dict).sort(([keyA], [keyB]) => {
return keyA.localeCompare(keyB);
});
break;
default:
sortedArray = Object.entries(dict);
break;
}
return Object.fromEntries(sortedArray);
}
// Function that prepares the output of dictSorter based on the chosen display format
function displayFormater(
dict: { [key: string]: number },
displayFormat: DisplayFormat
): string[] {
const formattedOutput: string[] = [];
const total = Object.values(dict).reduce((acc, val) => acc + val, 0);
switch (displayFormat) {
case 'percentage':
Object.entries(dict).forEach(([key, value]) => {
formattedOutput.push(
`${key}: ${value} (${((value / total) * 100).toFixed(2)}%)`
);
});
break;
case 'total':
Object.entries(dict).forEach(([key, value]) => {
formattedOutput.push(`${key}: ${value} (${value} / ${total})`);
});
break;
case 'count':
Object.entries(dict).forEach(([key, value]) => {
formattedOutput.push(`${key}: ${value}`);
});
break;
}
return formattedOutput;
}
export function TopItemsList(
splitOperatorType: SplitOperatorType,
sortingMethod: SortingMethod,
displayFormat: DisplayFormat,
splitSeparator: string,
input: string,
deleteEmptyItems: boolean,
ignoreItemCase: boolean,
trimItems: boolean
): string {
let array: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input
.split(new RegExp(splitSeparator))
.filter((item) => item !== '');
break;
}
// Trim items if required
if (trimItems) {
array = array.map((item) => item.trim());
}
// Delete empty items after initial split
if (deleteEmptyItems) {
array = array.filter((item) => item !== '');
}
// Transform the array into dict
const unsortedDict = dictMaker(array, ignoreItemCase);
// Sort the list if required
const sortedDict = dictSorter(unsortedDict, sortingMethod);
// Format the output with desired format
const formattedOutput = displayFormater(sortedDict, displayFormat);
return formattedOutput.join('\n');
}

View File

@@ -0,0 +1,110 @@
import { describe, expect } from 'vitest';
import { findUniqueCompute } from './service';
describe('TopItemsList Function', () => {
test('should return unique items ignoring case sensitivity', () => {
const input = 'apple,banana,Apple,orange,Banana,apple';
const result = findUniqueCompute(
'symbol',
',',
'\n',
input,
true,
true,
false,
true
);
expect(result).toBe('orange');
});
test('should return unique items considering case sensitivity', () => {
const input = 'apple,banana,Apple,orange,Banana,apple';
const result = findUniqueCompute(
'symbol',
',',
'\n',
input,
true,
true,
true,
true
);
expect(result).toBe('banana\nApple\norange\nBanana');
});
test('should return all unique items ignoring case sensitivity', () => {
const input = 'apple,banana,Apple,orange,Banana,apple';
const result = findUniqueCompute(
'symbol',
',',
'\n',
input,
true,
true,
false,
false
);
expect(result).toBe('apple\nbanana\norange');
});
test('should return all unique items considering case sensitivity', () => {
const input = 'apple,banana,Apple,orange,Banana,apple';
const result = findUniqueCompute(
'symbol',
',',
'\n',
input,
true,
true,
true,
false
);
expect(result).toBe('apple\nbanana\nApple\norange\nBanana');
});
test('should handle empty items deletion', () => {
const input = 'apple,,banana, ,orange';
const result = findUniqueCompute(
'symbol',
',',
'\n',
input,
true,
true,
false,
false
);
expect(result).toBe('apple\nbanana\norange');
});
test('should handle trimming items', () => {
const input = ' apple , banana , orange ';
const result = findUniqueCompute(
'symbol',
',',
'\n',
input,
false,
false,
false,
false
);
expect(result).toBe(' apple \n banana \n orange ');
});
test('should handle regex split', () => {
const input = 'apple banana orange';
const result = findUniqueCompute(
'regex',
'\\s+',
'\n',
input,
false,
false,
false,
false
);
expect(result).toBe('apple\nbanana\norange');
});
});

View File

@@ -0,0 +1,158 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { findUniqueCompute, SplitOperatorType } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
splitSeparator: ',',
joinSeparator: '\\n',
deleteEmptyItems: true,
caseSensitive: false,
trimItems: true,
absolutelyUnique: false
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
export default function FindUnique() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const {
splitOperatorType,
splitSeparator,
joinSeparator,
deleteEmptyItems,
trimItems,
caseSensitive,
absolutelyUnique
} = optionsValues;
setResult(
findUniqueCompute(
splitOperatorType,
splitSeparator,
joinSeparator,
input,
deleteEmptyItems,
trimItems,
caseSensitive,
absolutelyUnique
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Unique items'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Input List Delimiter',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Output List Delimiter',
component: (
<Box>
<TextFieldWithDesc
value={values.joinSeparator}
onOwnChange={(value) => updateField('joinSeparator', value)}
/>
<CheckboxWithDesc
title={'Trim top list items'}
description={
'Remove leading and trailing spaces before comparing items'
}
checked={values.trimItems}
onChange={(value) => updateField('trimItems', value)}
/>
<CheckboxWithDesc
title={'Skip empty items'}
description={
"Don't include the empty list items in the output."
}
checked={values.deleteEmptyItems}
onChange={(value) => updateField('deleteEmptyItems', value)}
/>
</Box>
)
},
{
title: 'Unique Item Options',
component: (
<Box>
<CheckboxWithDesc
title={'Find Absolutely Unique Items'}
description={
'Display only those items of the list that exist in a single copy.'
}
checked={values.absolutelyUnique}
onChange={(value) => updateField('absolutelyUnique', value)}
/>
<CheckboxWithDesc
title={'Case Sensitive Items'}
description={
'Output items with different case as unique elements in the list.'
}
checked={values.caseSensitive}
onChange={(value) => updateField('caseSensitive', value)}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Find unique',
path: 'find-unique',
// image,
description: '',
shortDescription: '',
keywords: ['find', 'unique'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,64 @@
export type SplitOperatorType = 'symbol' | 'regex';
// Function that builds the unique items array handling caseSensitive and absolutelyUnique options
function uniqueListBuilder(
array: string[],
caseSensitive: boolean,
absolutelyUnique: boolean
): string[] {
const dict: { [key: string]: number } = {};
for (const item of array) {
const key = caseSensitive ? item : item.toLowerCase();
dict[key] = (dict[key] || 0) + 1;
}
if (absolutelyUnique) {
for (const [key, value] of Object.entries(dict)) {
if (value > 1) {
delete dict[key];
}
}
}
return Object.keys(dict);
}
export function findUniqueCompute(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
joinSeparator: string = '\n',
input: string,
deleteEmptyItems: boolean,
trimItems: boolean,
caseSensitive: boolean,
absolutelyUnique: boolean
): string {
let array: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input
.split(new RegExp(splitSeparator))
.filter((item) => item !== '');
break;
}
// Trim items if required
if (trimItems) {
array = array.map((item) => item.trim());
}
// Delete empty items after initial split
if (deleteEmptyItems) {
array = array.filter((item) => item !== '');
}
// Format the output with desired format
const uniqueListItems = uniqueListBuilder(
array,
caseSensitive,
absolutelyUnique
);
return uniqueListItems.join(joinSeparator);
}

View File

@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest';
import { groupList, SplitOperatorType } from './service';
describe('groupList', () => {
it('splits by symbol, groups, pads, and formats correctly', () => {
const input = 'a,b,c,d,e,f,g,h,i,j';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ',';
const groupNumber = 3;
const itemSeparator = '-';
const leftWrap = '[';
const rightWrap = ']';
const groupSeparator = ' | ';
const deleteEmptyItems = false;
const padNonFullGroup = true;
const paddingChar = 'x';
const expectedOutput = '[a-b-c] | [d-e-f] | [g-h-i] | [j-x-x]';
const result = groupList(
splitOperatorType,
splitSeparator,
input,
groupNumber,
itemSeparator,
leftWrap,
rightWrap,
groupSeparator,
deleteEmptyItems,
padNonFullGroup,
paddingChar
);
expect(result).toBe(expectedOutput);
});
it('handles regex split, no padding, and formats correctly', () => {
const input = 'a1b2c3d4e5f6g7h8i9j';
const splitOperatorType: SplitOperatorType = 'regex';
const splitSeparator = '\\d';
const groupNumber = 4;
const itemSeparator = ',';
const leftWrap = '(';
const rightWrap = ')';
const groupSeparator = ' / ';
const deleteEmptyItems = true;
const padNonFullGroup = false;
const expectedOutput = '(a,b,c,d) / (e,f,g,h) / (i,j)';
const result = groupList(
splitOperatorType,
splitSeparator,
input,
groupNumber,
itemSeparator,
leftWrap,
rightWrap,
groupSeparator,
deleteEmptyItems,
padNonFullGroup
);
expect(result).toBe(expectedOutput);
});
it('handles empty items removal and padd the last group with a z', () => {
const input = 'a,,b,,c,,d,,e,,';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ',';
const groupNumber = 2;
const itemSeparator = ':';
const leftWrap = '<';
const rightWrap = '>';
const groupSeparator = ' & ';
const deleteEmptyItems = true;
const padNonFullGroup = true;
const paddingChar = 'z';
const expectedOutput = '<a:b> & <c:d> & <e:z>';
const result = groupList(
splitOperatorType,
splitSeparator,
input,
groupNumber,
itemSeparator,
leftWrap,
rightWrap,
groupSeparator,
deleteEmptyItems,
padNonFullGroup,
paddingChar
);
expect(result).toBe(expectedOutput);
});
});

View File

@@ -0,0 +1,183 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { groupList, SplitOperatorType } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import { formatNumber } from '../../../../utils/number';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
splitSeparator: ',',
groupNumber: 2,
itemSeparator: ',',
leftWrap: '[',
rightWrap: ']',
groupSeparator: '\\n',
deleteEmptyItems: true,
padNonFullGroup: false,
paddingChar: '...'
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
export default function FindUnique() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const {
splitOperatorType,
splitSeparator,
groupNumber,
itemSeparator,
leftWrap,
rightWrap,
groupSeparator,
deleteEmptyItems,
padNonFullGroup,
paddingChar
} = optionsValues;
setResult(
groupList(
splitOperatorType,
splitSeparator,
input,
groupNumber,
itemSeparator,
leftWrap,
rightWrap,
groupSeparator,
deleteEmptyItems,
padNonFullGroup,
paddingChar
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Grouped items'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Input Item Separator',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Group Size and Separators',
component: (
<Box>
<TextFieldWithDesc
value={values.groupNumber}
description={'Number of items in a group'}
type={'number'}
onOwnChange={(value) =>
updateField('groupNumber', formatNumber(value, 1))
}
/>
<TextFieldWithDesc
value={values.itemSeparator}
description={'Item separator character'}
onOwnChange={(value) => updateField('itemSeparator', value)}
/>
<TextFieldWithDesc
value={values.groupSeparator}
description={'Group separator character'}
onOwnChange={(value) => updateField('groupSeparator', value)}
/>
<TextFieldWithDesc
value={values.leftWrap}
description={"Group's left wrap symbol."}
onOwnChange={(value) => updateField('leftWrap', value)}
/>
<TextFieldWithDesc
value={values.rightWrap}
description={"Group's right wrap symbol."}
onOwnChange={(value) => updateField('rightWrap', value)}
/>
</Box>
)
},
{
title: 'Empty Items and Padding',
component: (
<Box>
<CheckboxWithDesc
title={'Delete Empty Items'}
description={
"Ignore empty items and don't include them in the groups."
}
checked={values.deleteEmptyItems}
onChange={(value) => updateField('deleteEmptyItems', value)}
/>
<CheckboxWithDesc
title={'Pad Non-full Groups'}
description={
'Fill non-full groups with a custom item (enter below).'
}
checked={values.padNonFullGroup}
onChange={(value) => updateField('padNonFullGroup', value)}
/>
<TextFieldWithDesc
value={values.paddingChar}
description={
'Use this character or item to pad non-full groups.'
}
onOwnChange={(value) => updateField('paddingChar', value)}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Group',
path: 'group',
// image,
description: '',
shortDescription: '',
keywords: ['group'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,94 @@
export type SplitOperatorType = 'symbol' | 'regex';
// function that split the array into an array of subarray of desired length
function groupMaker(array: string[], groupNumber: number): string[][] {
const result: string[][] = [];
for (let i = 0; i < array.length; i += groupNumber) {
result.push(array.slice(i, i + groupNumber));
}
return result;
}
// function use to handle the case paddingNonFullGroup is enable
function groupFiller(
array: string[][],
groupNumber: number,
padNonFullGroup: boolean,
paddingChar: string = ''
): string[][] {
if (padNonFullGroup) {
const lastSubArray: string[] = array[array.length - 1];
if (lastSubArray.length < groupNumber) {
for (let i = lastSubArray.length; i < groupNumber; i++) {
lastSubArray.push(paddingChar);
}
}
array[array.length - 1] = lastSubArray;
}
return array;
}
// function that join with the item separator and wrap with left and right each subArray of the Array
function groupJoinerAndWrapper(
array: string[][],
itemSeparator: string = '',
leftWrap: string = '',
rightWrap: string = ''
): string[] {
return array.map((subArray) => {
return leftWrap + subArray.join(itemSeparator) + rightWrap;
});
}
export function groupList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
input: string,
groupNumber: number,
itemSeparator: string = '',
leftWrap: string = '',
rightWrap: string = '',
groupSeparator: string,
deleteEmptyItems: boolean,
padNonFullGroup: boolean,
paddingChar: string = ''
): string {
let array: string[];
let splitedArray: string[][];
let fullSplitedArray: string[][];
let result: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
// delete empty items after intial split
if (deleteEmptyItems) {
array = array.filter((item) => item !== '');
}
// split the input into an array of subArray with the desired length
splitedArray = groupMaker(array, groupNumber);
// fill the last subArray is PadNonFullGroup is enabled
fullSplitedArray = groupFiller(
splitedArray,
groupNumber,
padNonFullGroup,
paddingChar
);
// get the list of formated subArray with the item separator and left and right wrapper
result = groupJoinerAndWrapper(
fullSplitedArray,
itemSeparator,
leftWrap,
rightWrap
);
// finnaly join the group separator before returning
return result.join(groupSeparator);
}

View File

@@ -0,0 +1,25 @@
import { tool as listDuplicate } from './duplicate/meta';
import { tool as listUnwrap } from './unwrap/meta';
import { tool as listReverse } from './reverse/meta';
import { tool as listFindUnique } from './find-unique/meta';
import { tool as listFindMostPopular } from './find-most-popular/meta';
import { tool as listGroup } from './group/meta';
import { tool as listWrap } from './wrap/meta';
import { tool as listRotate } from './rotate/meta';
import { tool as listTruncate } from './truncate/meta';
import { tool as listShuffle } from './shuffle/meta';
import { tool as listSort } from './sort/meta';
export const listTools = [
listSort,
listUnwrap,
listReverse,
listFindUnique,
listFindMostPopular,
listGroup,
listWrap,
listRotate,
listShuffle,
listTruncate,
listDuplicate
];

View File

@@ -0,0 +1,105 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { reverseList, SplitOperatorType } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
splitSeparator: ',',
joinSeparator: '\\n'
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
export default function Reverse() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const { splitOperatorType, splitSeparator, joinSeparator } = optionsValues;
setResult(
reverseList(splitOperatorType, splitSeparator, joinSeparator, input)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Reversed list'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Splitter Mode',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
</Box>
)
},
{
title: 'Item Separator',
component: (
<Box>
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Output List Options',
component: (
<Box>
<TextFieldWithDesc
description={'Output list item separator.'}
value={values.joinSeparator}
onOwnChange={(val) => updateField('joinSeparator', val)}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Reverse',
path: 'reverse',
// image,
description: '',
shortDescription: '',
keywords: ['reverse'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,28 @@
import { describe, expect } from 'vitest';
import { reverseList } from './service';
describe('reverseList Function', () => {
test('should reverse items split by symbol', () => {
const input = 'apple,banana,orange';
const result = reverseList('symbol', ',', '\n', input);
expect(result).toBe('orange\nbanana\napple');
});
test('should reverse items split by regex', () => {
const input = 'apple banana orange';
const result = reverseList('regex', '\\s+', '\n', input);
expect(result).toBe('orange\nbanana\napple');
});
test('should handle empty input', () => {
const input = '';
const result = reverseList('symbol', ',', '\n', input);
expect(result).toBe('');
});
test('should handle join separator', () => {
const input = 'apple,banana,orange';
const result = reverseList('symbol', ',', ', ', input);
expect(result).toBe('orange, banana, apple');
});
});

View File

@@ -0,0 +1,23 @@
export type SplitOperatorType = 'symbol' | 'regex';
export function reverseList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
joinSeparator: string = '\n',
input: string
): string {
let array: string[] = [];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input
.split(new RegExp(splitSeparator))
.filter((item) => item !== '');
break;
}
const reversedList = array.reverse();
return reversedList.join(joinSeparator);
}

View File

@@ -0,0 +1,153 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { rotateList, SplitOperatorType } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { formatNumber } from '../../../../utils/number';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
input: '',
splitSeparator: ',',
joinSeparator: ',',
right: true,
step: 1
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
const rotationDirections: {
title: string;
description: string;
value: boolean;
}[] = [
{
title: 'Rotate forward',
description:
'Rotate list items to the right. (Down if a vertical column list.)',
value: true
},
{
title: 'Rotate backward',
description:
'Rotate list items to the left. (Up if a vertical column list.)',
value: false
}
];
export default function Rotate() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const { splitOperatorType, splitSeparator, joinSeparator, right, step } =
optionsValues;
setResult(
rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right,
step
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Rotated list'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Item split mode',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Rotation Direction and Count',
component: (
<Box>
{rotationDirections.map(({ title, description, value }) => (
<SimpleRadio
key={`${value}`}
onClick={() => updateField('right', value)}
title={title}
description={description}
checked={values.right === value}
/>
))}
<TextFieldWithDesc
description={'Number of items to rotate'}
value={values.step}
onOwnChange={(val) =>
updateField('step', formatNumber(val, 1))
}
/>
</Box>
)
},
{
title: 'Rotated List Joining Symbol',
component: (
<Box>
<TextFieldWithDesc
value={values.joinSeparator}
onOwnChange={(value) => updateField('joinSeparator', value)}
description={
'Enter the character that goes between items in the rotated list.'
}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Rotate',
path: 'rotate',
// image,
description: '',
shortDescription: '',
keywords: ['rotate'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest';
import { rotateList, SplitOperatorType } from './service';
describe('rotate function', () => {
it('should rotate right side if right is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const step = 1;
const right = true;
const result = rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right,
step
);
expect(result).toBe('mango apple pineaple lemon orange');
});
it('should rotate left side if right is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const step = 1;
const right = false;
const result = rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right,
step
);
expect(result).toBe('pineaple lemon orange mango apple');
});
it('should rotate left side with 2 step if right is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const step = 2;
const right = false;
const result = rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right,
step
);
expect(result).toBe('lemon orange mango apple pineaple');
});
it('should raise an error if step is negative', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const step = -2;
const right = false;
expect(() => {
rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right,
step
);
}).toThrowError('Rotation step must be greater than zero.');
});
it('should raise an error if step is undefined', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const right = false;
expect(() => {
rotateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
right
);
}).toThrowError('Rotation step contains non-digits.');
});
});

View File

@@ -0,0 +1,48 @@
export type SplitOperatorType = 'symbol' | 'regex';
function rotateArray(array: string[], step: number, right: boolean): string[] {
const length = array.length;
// Normalize the step to be within the bounds of the array length
const normalizedPositions = ((step % length) + length) % length;
if (right) {
// Rotate right
return array
.slice(-normalizedPositions)
.concat(array.slice(0, -normalizedPositions));
} else {
// Rotate left
return array
.slice(normalizedPositions)
.concat(array.slice(0, normalizedPositions));
}
}
export function rotateList(
splitOperatorType: SplitOperatorType,
input: string,
splitSeparator: string,
joinSeparator: string,
right: boolean,
step?: number
): string {
let array: string[];
let rotatedArray: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
if (step !== undefined) {
if (step <= 0) {
throw new Error('Rotation step must be greater than zero.');
}
rotatedArray = rotateArray(array, step, right);
return rotatedArray.join(joinSeparator);
}
throw new Error('Rotation step contains non-digits.');
}

View File

@@ -0,0 +1,119 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { shuffleList, SplitOperatorType } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { isNumber } from '../../../../utils/string';
const initialValues = {
splitOperatorType: 'symbol' as SplitOperatorType,
splitSeparator: ',',
joinSeparator: ',',
length: ''
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
export default function Shuffle() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const { splitOperatorType, splitSeparator, joinSeparator, length } =
optionsValues;
setResult(
shuffleList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
isNumber(length) ? Number(length) : undefined
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Shuffled list'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Input list separator',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={title}
description={description}
checked={values.splitOperatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Shuffled List Length',
component: (
<Box>
<TextFieldWithDesc
description={'Output this many random items'}
value={values.length}
onOwnChange={(val) => updateField('length', val)}
/>
</Box>
)
},
{
title: 'Shuffled List Separator',
component: (
<Box>
<TextFieldWithDesc
value={values.joinSeparator}
onOwnChange={(value) => updateField('joinSeparator', value)}
description={'Use this separator in the randomized list.'}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Shuffle',
path: 'shuffle',
// image,
description: '',
shortDescription: '',
keywords: ['shuffle'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,38 @@
export type SplitOperatorType = 'symbol' | 'regex';
// function that randomize the array
function shuffleArray(array: string[]): string[] {
const shuffledArray = array.slice(); // Create a copy of the array
for (let i = shuffledArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
}
return shuffledArray;
}
export function shuffleList(
splitOperatorType: SplitOperatorType,
input: string,
splitSeparator: string,
joinSeparator: string,
length?: number // "?" is to handle the case the user let the input blank
): string {
let array: string[];
let shuffledArray: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
shuffledArray = shuffleArray(array);
if (length !== undefined) {
if (length <= 0) {
throw new Error('Length value must be a positive number.');
}
return shuffledArray.slice(0, length).join(joinSeparator);
}
return shuffledArray.join(joinSeparator);
}

View File

@@ -0,0 +1,89 @@
import { describe, expect, it } from 'vitest';
import { shuffleList, SplitOperatorType } from './service';
describe('shuffle function', () => {
it('should be a 4 length list if no length value defined ', () => {
const input: string = 'apple, pineaple, lemon, orange';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const result = shuffleList(
splitOperatorType,
input,
splitSeparator,
joinSeparator
);
expect(result.split(joinSeparator).length).toBe(4);
});
it('should be a 2 length list if length value is set to 2', () => {
const input: string = 'apple, pineaple, lemon, orange';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const length = 2;
const result = shuffleList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
length
);
console.log(result);
expect(result.split(joinSeparator).length).toBe(2);
});
it('should be a 4 length list if length value is set to 99', () => {
const input: string = 'apple, pineaple, lemon, orange';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const length = 99;
const result = shuffleList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
length
);
console.log(result);
expect(result.split(joinSeparator).length).toBe(4);
});
it('should include a random element if length value is undefined', () => {
const input: string = 'apple, pineaple, lemon, orange';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const result = shuffleList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
length
);
console.log(result);
expect(result.split(joinSeparator)).toContain('apple');
});
it('should return empty string if input is empty', () => {
const input: string = '';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const result = shuffleList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
length
);
console.log(result);
expect(result).toBe('');
});
});

View File

@@ -0,0 +1,165 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { Sort, SortingMethod, SplitOperatorType } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import SelectWithDesc from '@components/options/SelectWithDesc';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
sortingMethod: 'alphabetic' as SortingMethod,
increasing: true,
splitSeparator: ',',
joinSeparator: ',',
removeDuplicated: false,
caseSensitive: false
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
joinSeparator,
splitSeparator,
increasing,
caseSensitive,
removeDuplicated,
sortingMethod
} = optionsValues;
setResult(
Sort(
sortingMethod,
splitSeparatorType,
input,
increasing,
splitSeparator,
joinSeparator,
removeDuplicated,
caseSensitive
)
);
};
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Input list'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Sorted list'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Input item separator',
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitSeparatorType', type)}
title={title}
description={description}
checked={values.splitSeparatorType === type}
/>
))}
<TextFieldWithDesc
description={'Set a delimiting symbol or regular expression.'}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
</Box>
)
},
{
title: 'Sort method',
component: (
<Box>
<SelectWithDesc
selected={values.sortingMethod}
options={[
{ label: 'Sort Alphabetically', value: 'alphabetic' },
{ label: 'Sort Numerically', value: 'numeric' },
{ label: 'Sort by Length', value: 'length' }
]}
onChange={(value) => updateField('sortingMethod', value)}
description={'Select a sorting method.'}
/>
<SelectWithDesc
selected={values.increasing}
options={[
{ label: 'Increasing order', value: true },
{ label: 'Decreasing order', value: false }
]}
onChange={(value) => {
updateField('increasing', value);
}}
description={'Select a sorting order.'}
/>
<CheckboxWithDesc
title={'Case Sensitive Sort'}
description={
'Sort uppercase and lowercase items separately. Capital letters precede lowercase letters in an ascending list. (Works only in alphabetical sorting mode.)'
}
checked={values.caseSensitive}
onChange={(val) => updateField('caseSensitive', val)}
/>
</Box>
)
},
{
title: 'Sorted item properties',
component: (
<Box>
<TextFieldWithDesc
description={
'Use this symbol as a joiner between items in a sorted list.'
}
value={values.joinSeparator}
onOwnChange={(val) => updateField('joinSeparator', val)}
/>
<CheckboxWithDesc
title={'Remove duplicates'}
description={'Delete duplicate list items.'}
checked={values.removeDuplicated}
onChange={(val) => updateField('removeDuplicated', val)}
/>
</Box>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Sort',
path: 'sort',
// image,
description:
'This is a super simple browser-based application that sorts items in a list and arranges them in increasing or decreasing order. You can sort the items alphabetically, numerically, or by their length. You can also remove duplicate and empty items, as well as trim individual items that have whitespace around them. You can use any separator character to separate the input list items or alternatively use a regular expression to separate them. Additionally, you can create a new delimiter for the sorted output list.',
shortDescription: 'Quickly sort a list',
keywords: ['sort'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,123 @@
import { isNumber } from 'utils/string';
export type SortingMethod = 'numeric' | 'alphabetic' | 'length';
export type SplitOperatorType = 'symbol' | 'regex';
// utils function that choose the way of numeric sorting mixed types of array
function customNumericSort(a: string, b: string, increasing: boolean): number {
const formattedA = isNumber(a) ? Number(a) : a;
const formattedB = isNumber(b) ? Number(b) : b;
if (typeof formattedA === 'number' && typeof formattedB === 'number') {
return increasing ? formattedA - formattedB : formattedB - formattedA;
} else if (typeof formattedA === 'string' && typeof formattedB === 'string') {
return formattedA.localeCompare(formattedB); // Lexicographical comparison for strings
} else if (typeof formattedA === 'number' && typeof formattedB === 'string') {
return -1; // Numbers before strings
} else {
return 1; // Strings after numbers
}
}
export function numericSort(
array: string[], // array we build after parsing the input
increasing: boolean,
joinSeparator: string,
removeDuplicated: boolean // the value if the checkbox has been selected 1 else 0
) {
array.sort((a, b) => customNumericSort(a, b, increasing));
if (removeDuplicated) {
array = array.filter((item, index) => array.indexOf(item) === index);
}
return array.join(joinSeparator);
}
// utils function that choose the way of numeric sorting mixed types of array
function customLengthSort(a: string, b: string, increasing: boolean): number {
return increasing ? a.length - b.length : b.length - a.length;
}
export function lengthSort(
array: string[], // array we build after parsing the input
increasing: boolean, // select value has to be increasing for increasing order and decreasing for decreasing order
joinSeparator: string,
removeDuplicated: boolean // the value if the checkbox has been selected 1 else 0
) {
array.sort((a, b) => customLengthSort(a, b, increasing));
if (removeDuplicated) {
array = array.filter((item, index) => array.indexOf(item) === index);
}
return array.join(joinSeparator);
}
// Utils function that chooses the way of alphabetic sorting mixed types of array
function customAlphabeticSort(
a: string,
b: string,
caseSensitive: boolean
): number {
if (!caseSensitive) {
// Case-insensitive comparison
return a.toLowerCase().localeCompare(b.toLowerCase());
} else {
// Case-sensitive comparison
return a.charCodeAt(0) - b.charCodeAt(0);
}
}
export function alphabeticSort(
array: string[], // array we build after parsing the input
increasing: boolean, // select value has to be "increasing" for increasing order and "decreasing" for decreasing order
joinSeparator: string,
removeDuplicated: boolean, // the value if the checkbox has been selected 1 else 0
caseSensitive: boolean // the value if the checkbox has been selected 1 else 0
) {
array.sort((a, b) => customAlphabeticSort(a, b, caseSensitive));
if (!increasing) {
array.reverse();
}
if (removeDuplicated) {
array = array.filter((item, index) => array.indexOf(item) === index);
}
return array.join(joinSeparator);
}
// main function
export function Sort(
sortingMethod: SortingMethod,
splitOperatorType: SplitOperatorType,
input: string,
increasing: boolean,
splitSeparator: string,
joinSeparator: string,
removeDuplicated: boolean,
caseSensitive: boolean
) {
let array: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
let result: string;
switch (sortingMethod) {
case 'numeric':
result = numericSort(array, increasing, joinSeparator, removeDuplicated);
break;
case 'length':
result = lengthSort(array, increasing, joinSeparator, removeDuplicated);
break;
case 'alphabetic':
result = alphabeticSort(
array,
increasing,
joinSeparator,
removeDuplicated,
caseSensitive
);
break;
}
return result;
}

View File

@@ -0,0 +1,352 @@
// Import necessary modules and functions
import { describe, expect, it } from 'vitest';
import {
alphabeticSort,
lengthSort,
numericSort,
Sort,
SortingMethod,
SplitOperatorType
} from './service';
// Define test cases for the numericSort function
describe('numericSort function', () => {
it('should sort a list in increasing order with comma separator not removeduplicated elements', () => {
const array: string[] = ['9', '8', '7', '4', '2', '2', '5'];
const increasing: boolean = true;
const separator = ', ';
const removeDuplicated: boolean = false;
const result = numericSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('2, 2, 4, 5, 7, 8, 9');
});
it('should sort a list in decreasing order with " - " separator and remove duplicated elements', () => {
const array: string[] = ['2', '4', '4', '9', '6', '6', '7'];
const increasing: boolean = false;
const separator = ' - ';
const removeDuplicated: boolean = true;
const result = numericSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('9 - 7 - 6 - 4 - 2');
});
it('should sort a list with numbers and characters and remove duplicated elements', () => {
const array: string[] = ['d', 'd', 'n', 'p', 'h', 'h', '6', '9', '7', '5'];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const result = numericSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('5 6 7 9 d h n p');
});
// Define test cases for the lengthSort function
describe('lengthSort function', () => {
it('should sort a list of number by length in increasing order with comma separator ', () => {
const array: string[] = ['415689521', '3', '126', '12', '1523'];
const increasing: boolean = true;
const separator = ', ';
const removeDuplicated: boolean = false;
const result = lengthSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('3, 12, 126, 1523, 415689521');
});
it('should sort a list of number by length in increasing order and remove duplicated elements ', () => {
const array: string[] = [
'415689521',
'3',
'3',
'126',
'12',
'12',
'1523'
];
const increasing: boolean = true;
const separator = ', ';
const removeDuplicated: boolean = true;
const result = lengthSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('3, 12, 126, 1523, 415689521');
});
it('should sort a mixed array by length in increasing order ', () => {
const array: string[] = [
'ddd',
'd',
'nfg',
'p',
'h',
'h',
'6555',
'9',
'7',
'5556'
];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const result = lengthSort(array, increasing, separator, removeDuplicated);
expect(result).toBe('d p h 9 7 ddd nfg 6555 5556');
});
});
// Define test cases for the alphabeticSort function
describe('alphabeticSort function', () => {
// NON CASE SENSITIVE TEST
it('should sort a list of string in increasing order with comma separator ', () => {
const array: string[] = ['apple', 'pineaple', 'lemon', 'orange'];
const increasing: boolean = true;
const separator = ', ';
const removeDuplicated: boolean = false;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('apple, lemon, orange, pineaple');
});
it('should sort a list of string in decreasing order with comma separator ', () => {
const array: string[] = ['apple', 'pineaple', 'lemon', 'orange'];
const increasing: boolean = false;
const separator = ', ';
const removeDuplicated: boolean = false;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('pineaple, orange, lemon, apple');
});
it('should sort a list of string and symbols (uppercase and lower) in increasing order with comma separator ', () => {
const array: string[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
'1',
'9',
'@',
'+'
];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('@ + 1 9 Apple lemon Orange pineaple');
});
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
const array: string[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
'1',
'9',
'@',
'+'
];
const increasing: boolean = false;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = false;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('pineaple Orange lemon Apple 9 1 + @');
});
// CASE SENSITIVE TEST
it('should sort a list of string (uppercase) in decreasing order with comma separator ', () => {
const array: string[] = ['Apple', 'Pineaple', 'Lemon', 'Orange'];
const increasing: boolean = false;
const separator = ' ';
const removeDuplicated: boolean = false;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('Pineaple Orange Lemon Apple');
});
it('should sort a list of string (uppercase and lowercase) in increasing order with comma separator ', () => {
const array: string[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
'1',
'9'
];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('1 9 Apple Orange lemon pineaple');
});
it('should sort a list of string (uppercase and lower) in decreasing order with comma separator ', () => {
const array: string[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
'1',
'9'
];
const increasing: boolean = false;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('pineaple lemon Orange Apple 9 1');
});
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
const array: string[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
'1',
'9',
'@',
'+'
];
const increasing: boolean = true;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('+ 1 9 @ Apple Orange lemon pineaple');
});
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
const array: string[] = [
'Apple',
'pineaple',
'lemon',
'Orange',
'1',
'9',
'@',
'+'
];
const increasing: boolean = false;
const separator = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = alphabeticSort(
array,
increasing,
separator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('pineaple lemon Orange Apple @ 9 1 +');
});
});
// Define test cases for the lengthSort function
describe('main function', () => {
it('should do everything alph', () => {
const sortingMethod: SortingMethod = 'alphabetic';
const splitOperatorType: SplitOperatorType = 'symbol';
const input: string = 'Apple pineaple lemon Orange 1 9 @ +';
const increasing: boolean = true;
const splitSeparator: string = ' ';
const joinSeparator: string = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = Sort(
sortingMethod,
splitOperatorType,
input,
increasing,
splitSeparator,
joinSeparator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('+ 1 9 @ Apple Orange lemon pineaple');
});
it('should do everything numeric', () => {
const sortingMethod: SortingMethod = 'numeric';
const splitOperatorType: SplitOperatorType = 'symbol';
const input: string = '1 6 9 4 6 7 3 5 8';
const increasing: boolean = true;
const splitSeparator: string = ' ';
const joinSeparator: string = ' ';
const removeDuplicated: boolean = true;
const caseSensitive: boolean = true;
const result = Sort(
sortingMethod,
splitOperatorType,
input,
increasing,
splitSeparator,
joinSeparator,
removeDuplicated,
caseSensitive
);
expect(result).toBe('1 3 4 5 6 7 8 9');
});
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Truncate() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Truncate',
path: 'truncate',
// image,
description: '',
shortDescription: '',
keywords: ['truncate'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,31 @@
export type SplitOperatorType = 'symbol' | 'regex';
export function truncateList(
splitOperatorType: SplitOperatorType,
input: string,
splitSeparator: string,
joinSeparator: string,
end: boolean,
length?: number
): string {
let array: string[];
let truncatedArray: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
if (length !== undefined) {
if (length < 0) {
throw new Error('Length value must be a positive number.');
}
truncatedArray = end
? array.slice(0, length)
: array.slice(array.length - length, array.length);
return truncatedArray.join(joinSeparator);
}
throw new Error("Length value isn't a value number.");
}

View File

@@ -0,0 +1,183 @@
import { describe, expect, it } from 'vitest';
import { SplitOperatorType, truncateList } from './service';
describe('truncate function', () => {
it('should remove at the end (one element) if end is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = true;
const length = 3;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('apple pineaple lemon');
});
it('should return 3 elements from the start if end is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = true;
const length = 3;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('apple pineaple lemon');
});
it('should return 3 elements from the start if end is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = true;
const length = 3;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('apple pineaple lemon');
});
it('should return 3 elements from the end if end is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = false;
const length = 3;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('lemon orange mango');
});
it('should return a void string if length is set to 0', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = false;
const length = 0;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('');
});
it('should return an element (first) string if length is set to 1 and end is set to true', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = true;
const length = 1;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('apple');
});
it('should return an element (last) string if length is set to 1 and end is set to false', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = false;
const length = 1;
const result = truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
expect(result).toBe('mango');
});
it('should throw an error if the length value is negative', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = false;
const length = -5;
expect(() => {
truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end,
length
);
}).toThrow('Length value must be a positive number.');
});
it('should throw an error if the length value is left blank', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ' ';
const end = false;
expect(() => {
truncateList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
end
);
}).toThrow("Length value isn't a value number.");
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Unwrap() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Unwrap',
path: 'unwrap',
// image,
description: '',
shortDescription: '',
keywords: ['unwrap'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,69 @@
export type SplitOperatorType = 'symbol' | 'regex';
function leftUnwrap(
row: string,
left: string = '',
multiLevel: boolean
): string {
if (left === '') return row; // Prevent infinite loop if left is an empty string
while (row.startsWith(left)) {
row = row.slice(left.length);
if (!multiLevel) {
break;
}
}
return row;
}
function rightUnwrap(
row: string,
right: string = '',
multiLevel: boolean
): string {
if (right === '') return row; // Prevent infinite loop if right is an empty string
while (row.endsWith(right)) {
row = row.slice(0, row.length - right.length);
if (!multiLevel) {
break;
}
}
return row;
}
export function unwrapList(
splitOperatorType: SplitOperatorType,
input: string,
splitSeparator: string,
joinSeparator: string,
deleteEmptyItems: boolean,
multiLevel: boolean,
trimItems: boolean,
left: string = '',
right: string = ''
): string {
let array: string[];
let unwrappedArray: string[] = [];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
if (deleteEmptyItems) {
array = array.filter(Boolean);
}
// for each element of array unwrap left side then right side and push the result to a final array
for (let row of array) {
row = leftUnwrap(row, left, multiLevel);
row = rightUnwrap(row, right, multiLevel);
unwrappedArray.push(row);
}
// trim items if needed
if (trimItems) {
unwrappedArray = unwrappedArray.map((item) => item.trim());
}
return unwrappedArray.join(joinSeparator);
}

View File

@@ -0,0 +1,170 @@
import { describe, expect, it } from 'vitest';
import { unwrapList } from './service';
describe('unwrapList function', () => {
it('should unwrap elements correctly with symbol split', () => {
const input = '##Hello##\n##World##';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World');
});
it('should unwrap elements correctly with regex split', () => {
const input = '##Hello##||##World##';
const result = unwrapList(
'regex',
input,
'\\|\\|',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World');
});
it('should handle multiple levels of unwrapping', () => {
const input = '###Hello###';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello');
});
it('should handle single level of unwrapping', () => {
const input = '###Hello###';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
true,
false,
true,
'#',
'#'
);
expect(result).toBe('##Hello##');
});
it('should delete empty items', () => {
const input = '##Hello##\n\n##World##';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World');
});
it('should keep empty items if deleteEmptyItems is false', () => {
const input = '##Hello##\n\n##World##';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
false,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World');
});
it('should trim items', () => {
const input = '## Hello ##\n## World ##';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World');
});
it('should handle no left or right unwrapping', () => {
const input = 'Hello\nWorld';
const result = unwrapList('symbol', input, '\n', ' ', true, true, true);
expect(result).toBe('Hello World');
});
it('should handle mixed levels of unwrapping', () => {
const input = '###Hello##\n#World###';
const result = unwrapList(
'symbol',
input,
'\n',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World');
});
it('should handle complex regex split', () => {
const input = '##Hello##||###World###||####Test####';
const result = unwrapList(
'regex',
input,
'\\|\\|',
' ',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello World Test');
});
it('should handle different joinSeparator', () => {
const input = '##Hello##\n##World##';
const result = unwrapList(
'symbol',
input,
'\n',
'-',
true,
true,
true,
'#',
'#'
);
expect(result).toBe('Hello-World');
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Wrap() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('list', {
name: 'Wrap',
path: 'wrap',
// image,
description: '',
shortDescription: '',
keywords: ['wrap'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,31 @@
export type SplitOperatorType = 'symbol' | 'regex';
function wrap(array: string[], left: string, right: string): string[] {
return array.map((element) => left + element + right);
}
export function wrapList(
splitOperatorType: SplitOperatorType,
input: string,
splitSeparator: string,
joinSeparator: string,
deleteEmptyItems: boolean,
left: string = '',
right: string = ''
): string {
let array: string[];
let wrappedArray: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
break;
case 'regex':
array = input.split(new RegExp(splitSeparator));
break;
}
if (deleteEmptyItems) {
array = array.filter(Boolean);
}
wrappedArray = wrap(array, left, right);
return wrappedArray.join(joinSeparator);
}

View File

@@ -0,0 +1,132 @@
import { describe, expect, it } from 'vitest';
import { SplitOperatorType, wrapList } from './service';
describe('wrap function', () => {
it('should return the same input if no left and right are blanked', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ', ';
const deleteEmptyItems = false;
const result = wrapList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
deleteEmptyItems
);
expect(result).toBe('apple, pineaple, lemon, orange, mango');
});
it('should append to left if defined', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ', ';
const left = 'the ';
const deleteEmptyItems = false;
const result = wrapList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
deleteEmptyItems,
left
);
expect(result).toBe(
'the apple, the pineaple, the lemon, the orange, the mango'
);
});
it('should append to right if defined', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ', ';
const left = '';
const right = 'z';
const deleteEmptyItems = false;
const result = wrapList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
deleteEmptyItems,
left,
right
);
expect(result).toBe('applez, pineaplez, lemonz, orangez, mangoz');
});
it('should append to both side if both defined', () => {
const input: string = 'apple, pineaple, lemon, orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ', ';
const deleteEmptyItems = false;
const left = 'K';
const right = 'z';
const result = wrapList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
deleteEmptyItems,
left,
right
);
expect(result).toBe('Kapplez, Kpineaplez, Klemonz, Korangez, Kmangoz');
});
it('should append to both side if both defined and not delete empty items', () => {
const input: string = 'apple, pineaple, lemon, orange, mango, ';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ', ';
const deleteEmptyItems = false;
const left = 'K';
const right = 'z';
const result = wrapList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
deleteEmptyItems,
left,
right
);
expect(result).toBe('Kapplez, Kpineaplez, Klemonz, Korangez, Kmangoz, Kz');
});
it('should append to both side if both defined and delete empty items', () => {
const input: string = 'apple, pineaple, lemon, , orange, mango';
const splitOperatorType: SplitOperatorType = 'symbol';
const splitSeparator = ', ';
const joinSeparator = ', ';
const deleteEmptyItems = true;
const left = 'K';
const right = 'z';
const result = wrapList(
splitOperatorType,
input,
splitSeparator,
joinSeparator,
deleteEmptyItems,
left,
right
);
expect(result).toBe('Kapplez, Kpineaplez, Klemonz, Korangez, Kmangoz');
});
});

View File

@@ -0,0 +1,76 @@
// Import necessary modules and functions
import { describe, expect, it } from 'vitest';
import { listOfIntegers } from './service';
// Define test cases for the listOfIntegers function
describe('listOfIntegers function', () => {
it('should generate a list of integers with comma separator', () => {
const initialValue = 1;
const step = 2;
const count = 5;
const separator = ', ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('1, 3, 5, 7, 9');
});
it('should generate a list of integers with dash separator', () => {
const initialValue = 0;
const step = 3;
const count = 4;
const separator = ' - ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('0 - 3 - 6 - 9');
});
it('should handle negative initial value and step', () => {
const initialValue = -10;
const step = -2;
const count = 5;
const separator = ' ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('-10 -12 -14 -16 -18');
});
it('should handle negative initial value and positive step', () => {
const initialValue = -10;
const step = 2;
const count = 5;
const separator = ' ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('-10 -8 -6 -4 -2');
});
it('should float value', () => {
const initialValue = -10;
const step = 2.5;
const count = 5;
const separator = ' ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('-10 -7.5 -5 -2.5 0');
});
it('should generate a constant sequence if the step is 0', () => {
const initialValue = 1;
const step = 0;
const count = 5;
const separator = ' ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('1 1 1 1 1');
});
it('should generate a constant sequence if the step is 0', () => {
const initialValue = 1;
const step = 0;
const count = 5;
const separator = ' ';
const result = listOfIntegers(initialValue, count, step, separator);
expect(result).toBe('1 1 1 1 1');
});
});

View File

@@ -0,0 +1,79 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { listOfIntegers } from './service';
import ToolInputAndResult from '@components/ToolInputAndResult';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
const initialValues = {
firstValue: '1',
numberOfNumbers: '10',
step: '1',
separator: '\\n'
};
export default function SplitText() {
const [result, setResult] = useState<string>('');
return (
<Box>
<ToolInputAndResult
result={<ToolTextResult title={'Total'} value={result} />}
/>
<ToolOptions
getGroups={({ values, updateField }) => [
{
title: 'Arithmetic sequence option',
component: (
<Box>
<TextFieldWithDesc
description={'Start sequence from this number.'}
value={values.firstValue}
onOwnChange={(val) => updateField('firstValue', val)}
type={'number'}
/>
<TextFieldWithDesc
description={'Increase each element by this amount'}
value={values.step}
onOwnChange={(val) => updateField('step', val)}
type={'number'}
/>
<TextFieldWithDesc
description={'Number of elements in sequence.'}
value={values.numberOfNumbers}
onOwnChange={(val) => updateField('numberOfNumbers', val)}
type={'number'}
/>
</Box>
)
},
{
title: 'Separator',
component: (
<TextFieldWithDesc
description={
'Separate elements in the arithmetic sequence by this character.'
}
value={values.separator}
onOwnChange={(val) => updateField('separator', val)}
/>
)
}
]}
compute={(optionsValues) => {
const { firstValue, numberOfNumbers, separator, step } =
optionsValues;
setResult(
listOfIntegers(
Number(firstValue),
Number(numberOfNumbers),
Number(step),
separator
)
);
}}
initialValues={initialValues}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('number', {
name: 'Generate numbers',
path: 'generate',
shortDescription: 'Quickly calculate a list of integers in your browser',
// image,
description:
'Quickly calculate a list of integers in your browser. To get your list, just specify the first integer, change value and total count in the options below, and this utility will generate that many integers',
keywords: ['generate'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,13 @@
export function listOfIntegers(
first_value: number,
number_of_numbers: number,
step: number,
separator: string
) {
const result: number[] = [];
for (let i: number = 0; i < number_of_numbers; i++) {
const value: number = first_value + i * step;
result.push(value);
}
return result.join(separator);
}

View File

@@ -0,0 +1,4 @@
import { tool as numberSum } from './sum/meta';
import { tool as numberGenerate } from './generate/meta';
export const numberTools = [numberSum, numberGenerate];

View File

@@ -0,0 +1,113 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { compute, NumberExtractionType } from './service';
import RadioWithTextField from '@components/options/RadioWithTextField';
import SimpleRadio from '@components/options/SimpleRadio';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
const initialValues = {
extractionType: 'smart' as NumberExtractionType,
separator: '\\n',
printRunningSum: false
};
const extractionTypes: {
title: string;
description: string;
type: NumberExtractionType;
withTextField: boolean;
textValueAccessor?: keyof typeof initialValues;
}[] = [
{
title: 'Smart sum',
description: 'Auto detect numbers in the input.',
type: 'smart',
withTextField: false
},
{
title: 'Number Delimiter',
type: 'delimiter',
description:
'Input SeparatorCustomize the number separator here. (By default a line break.)',
withTextField: true,
textValueAccessor: 'separator'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Total'} value={result} />}
/>
<ToolOptions
getGroups={({ values, updateField }) => [
{
title: 'Number extraction',
component: extractionTypes.map(
({
title,
description,
type,
withTextField,
textValueAccessor
}) =>
withTextField ? (
<RadioWithTextField
key={type}
checked={type === values.extractionType}
title={title}
fieldName={'extractionType'}
description={description}
value={
textValueAccessor
? values[textValueAccessor].toString()
: ''
}
onRadioClick={() => updateField('extractionType', type)}
onTextChange={(val) =>
textValueAccessor
? updateField(textValueAccessor, val)
: null
}
/>
) : (
<SimpleRadio
key={title}
onClick={() => updateField('extractionType', type)}
checked={values.extractionType === type}
description={description}
title={title}
/>
)
)
},
{
title: 'Running Sum',
component: (
<CheckboxWithDesc
title={'Print Running Sum'}
description={"Display the sum as it's calculated step by step."}
checked={values.printRunningSum}
onChange={(value) => updateField('printRunningSum', value)}
/>
)
}
]}
compute={(optionsValues, input) => {
const { extractionType, printRunningSum, separator } = optionsValues;
setResult(compute(input, extractionType, printRunningSum, separator));
}}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('number', {
name: 'Number Sum Calculator',
path: 'sum',
// image,
description:
'Quickly calculate the sum of numbers in your browser. To get your sum, just enter your list of numbers in the input field, adjust the separator between the numbers in the options below, and this utility will add up all these numbers.',
shortDescription: 'Quickly sum numbers',
keywords: ['sum'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,37 @@
export type NumberExtractionType = 'smart' | 'delimiter';
function getAllNumbers(text: string): number[] {
const regex = /\d+/g;
const matches = text.match(regex);
return matches ? matches.map(Number) : [];
}
export const compute = (
input: string,
extractionType: NumberExtractionType,
printRunningSum: boolean,
separator: string
): string => {
let numbers: number[] = [];
if (extractionType === 'smart') {
numbers = getAllNumbers(input);
} else {
const parts = input.split(separator);
// Filter out and convert parts that are numbers
numbers = parts
.filter((part) => !isNaN(Number(part)) && part.trim() !== '')
.map(Number);
}
if (printRunningSum) {
let result: string = '';
let sum: number = 0;
for (const i of numbers) {
sum = sum + i;
result = result + sum + '\n';
}
return result;
} else
return numbers
.reduce((previousValue, currentValue) => previousValue + currentValue, 0)
.toString();
};

View File

@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
it('should correctly sum numbers in smart extraction mode', () => {
const input = 'The 2 cats have 4 and 7 kittens';
const result = compute(input, 'smart', false, ',');
expect(result).toBe('13');
});
it('should correctly sum numbers with custom delimiter', () => {
const input = '2,4,7';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('13');
});
it('should return running sum in smart extraction mode', () => {
const input = 'The 2 cats have 4 and 7 kittens';
const result = compute(input, 'smart', true, ',');
expect(result).toBe('2\n6\n13\n');
});
it('should return running sum with custom delimiter', () => {
const input = '2,4,7';
const result = compute(input, 'delimiter', true, ',');
expect(result).toBe('2\n6\n13\n');
});
it('should handle empty input gracefully in smart mode', () => {
const input = '';
const result = compute(input, 'smart', false, ',');
expect(result).toBe('0');
});
it('should handle empty input gracefully in delimiter mode', () => {
const input = '';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('0');
});
it('should handle input with no numbers in smart mode', () => {
const input = 'There are no numbers here';
const result = compute(input, 'smart', false, ',');
expect(result).toBe('0');
});
it('should handle input with no numbers in delimiter mode', () => {
const input = 'a,b,c';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('0');
});
it('should ignore non-numeric parts in delimiter mode', () => {
const input = '2,a,4,b,7';
const result = compute(input, 'delimiter', false, ',');
expect(result).toBe('13');
});
it('should handle different separators', () => {
const input = '2;4;7';
const result = compute(input, 'delimiter', false, ';');
expect(result).toBe('13');
});
});

View File

@@ -0,0 +1,66 @@
import { describe, expect } from 'vitest';
import { createPalindrome, createPalindromeList } from './service';
describe('createPalindrome', () => {
test('should create palindrome by reversing the entire string', () => {
const input = 'hello';
const result = createPalindrome(input, true);
expect(result).toBe('helloolleh');
});
test('should create palindrome by reversing the string excluding the last character', () => {
const input = 'hello';
const result = createPalindrome(input, false);
expect(result).toBe('hellolleh');
});
test('should return an empty string if input is empty', () => {
const input = '';
const result = createPalindrome(input, true);
expect(result).toBe('');
});
});
describe('createPalindromeList', () => {
test('should create palindrome for single-line input', () => {
const input = 'hello';
const result = createPalindromeList(input, true, false);
expect(result).toBe('helloolleh');
});
test('should create palindrome for single-line input considering trailing spaces', () => {
const input = 'hello ';
const result = createPalindromeList(input, true, false);
expect(result).toBe('hello olleh');
});
test('should create palindrome for single-line input ignoring trailing spaces if lastChar is set to false', () => {
const input = 'hello ';
const result = createPalindromeList(input, true, false);
expect(result).toBe('hello olleh');
});
test('should create palindrome for multi-line input', () => {
const input = 'hello\nworld';
const result = createPalindromeList(input, true, true);
expect(result).toBe('helloolleh\nworlddlrow');
});
test('should create palindrome for no multi-line input', () => {
const input = 'hello\nworld\n';
const result = createPalindromeList(input, true, false);
expect(result).toBe('hello\nworld\n\ndlrow\nolleh');
});
test('should handle multi-line input with lastChar set to false', () => {
const input = 'hello\nworld';
const result = createPalindromeList(input, false, true);
expect(result).toBe('hellolleh\nworldlrow');
});
test('should return an empty string if input is empty', () => {
const input = '';
const result = createPalindromeList(input, true, false);
expect(result).toBe('');
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function CreatePalindrome() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'Create palindrome',
path: 'create-palindrome',
// image,
description: '',
shortDescription: '',
keywords: ['create', 'palindrome'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,36 @@
import { reverseString } from 'utils/string';
export function createPalindrome(
input: string,
lastChar: boolean // only checkbox is need here to handle it [instead of two combo boxes]
) {
if (!input) return '';
let result: string;
let reversedString: string;
// reverse the whole input if lastChar enabled
reversedString = lastChar
? reverseString(input)
: reverseString(input.slice(0, -1));
result = input.concat(reversedString);
return result;
}
export function createPalindromeList(
input: string,
lastChar: boolean,
multiLine: boolean
): string {
if (!input) return '';
let array: string[];
const result: string[] = [];
if (!multiLine) return createPalindrome(input, lastChar);
else {
array = input.split('\n');
for (const word of array) {
result.push(createPalindrome(word, lastChar));
}
}
return result.join('\n');
}

View File

@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import { extractSubstring } from './service';
describe('extractSubstring', () => {
it('should extract a substring from single-line input', () => {
const input = 'hello world';
const result = extractSubstring(input, 1, 4, false, false);
expect(result).toBe('hell');
});
it('should extract and reverse a substring from single-line input', () => {
const input = 'hello world';
const result = extractSubstring(input, 1, 5, false, true);
expect(result).toBe('olleh');
});
it('should extract substrings from multi-line input', () => {
const input = 'hello\nworld';
const result = extractSubstring(input, 1, 5, true, false);
expect(result).toBe('hello\nworld');
});
it('should extract and reverse substrings from multi-line input', () => {
const input = 'hello\nworld';
const result = extractSubstring(input, 1, 4, true, true);
expect(result).toBe('lleh\nlrow');
});
it('should handle empty input', () => {
const input = '';
const result = extractSubstring(input, 1, 5, false, false);
expect(result).toBe('');
});
it('should handle start and length out of bounds', () => {
const input = 'hello';
const result = extractSubstring(input, 10, 5, false, false);
expect(result).toBe('');
});
it('should handle negative start and length', () => {
expect(() => extractSubstring('hello', -1, 5, false, false)).toThrow(
'Start index must be greater than zero.'
);
expect(() => extractSubstring('hello', 1, -5, false, false)).toThrow(
'Length value must be greater than or equal to zero.'
);
});
it('should handle zero length', () => {
const input = 'hello';
const result = extractSubstring(input, 1, 0, false, false);
expect(result).toBe('');
});
it('should work', () => {
const input = 'je me nomme king\n22 est mon chiffre';
const result = extractSubstring(input, 12, 7, true, false);
expect(result).toBe(' king\nchiffre');
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function ExtractSubstring() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'Extract substring',
path: 'extract-substring',
// image,
description: '',
shortDescription: '',
keywords: ['extract', 'substring'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,36 @@
import { reverseString } from 'utils/string';
export function extractSubstring(
input: string,
start: number,
length: number,
multiLine: boolean,
reverse: boolean
): string {
if (!input) return '';
// edge Cases
if (start <= 0) throw new Error('Start index must be greater than zero.');
if (length < 0)
throw new Error('Length value must be greater than or equal to zero.');
if (length === 0) return '';
let array: string[];
let result: string[] = [];
const extract = (str: string, start: number, length: number): string => {
const end = start - 1 + length;
if (start - 1 >= str.length) return '';
return str.substring(start - 1, Math.min(end, str.length));
};
if (!multiLine) {
result.push(extract(input, start, length));
} else {
array = input.split('\n');
for (const word of array) {
result.push(extract(word, start, length));
}
}
result = reverse ? result.map((word) => reverseString(word)) : result;
return result.join('\n');
}

View File

@@ -0,0 +1,21 @@
import { tool as stringReverse } from './reverse/meta';
import { tool as stringRandomizeCase } from './randomize-case/meta';
import { tool as stringUppercase } from './uppercase/meta';
import { tool as stringExtractSubstring } from './extract-substring/meta';
import { tool as stringCreatePalindrome } from './create-palindrome/meta';
import { tool as stringPalindrome } from './palindrome/meta';
import { tool as stringToMorse } from './to-morse/meta';
import { tool as stringSplit } from './split/meta';
import { tool as stringJoin } from './join/meta';
export const stringTools = [
stringSplit,
stringJoin,
stringToMorse,
stringReverse,
stringRandomizeCase,
stringUppercase,
stringExtractSubstring,
stringCreatePalindrome,
stringPalindrome
];

View File

@@ -0,0 +1,190 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { mergeText } from './service';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
import ToolInfo from '@components/ToolInfo';
import Separator from '@components/Separator';
import Examples from '@components/examples/Examples';
const initialValues = {
joinCharacter: '',
deleteBlank: true,
deleteTrailing: true
};
const validationSchema = Yup.object().shape({
joinCharacter: Yup.string().required('Join character is required'),
deleteBlank: Yup.boolean().required('Delete blank is required'),
deleteTrailing: Yup.boolean().required('Delete trailing is required')
});
const mergeOptions = {
placeholder: 'Join Character',
description:
'Symbol that connects broken\n' + 'pieces of text. (Space by default.)\n',
accessor: 'joinCharacter' as keyof typeof initialValues
};
const blankTrailingOptions: {
title: string;
description: string;
accessor: keyof typeof initialValues;
}[] = [
{
title: 'Delete Blank Lines',
description: "Delete lines that don't have\n text symbols.\n",
accessor: 'deleteBlank'
},
{
title: 'Delete Trailing Spaces',
description: 'Remove spaces and tabs at\n the end of the lines.\n',
accessor: 'deleteTrailing'
}
];
const exampleCards = [
{
title: 'Merge a To-Do List',
description:
"In this example, we merge a bullet point list into one sentence, separating each item by the word 'and'. We also remove all empty lines and trailing spaces. If we didn't remove the empty lines, then they'd be joined with the separator word, making the separator word appear multiple times. If we didn't remove the trailing tabs and spaces, then they'd create extra spacing in the joined text and it wouldn't look nice.",
sampleText: `clean the house
go shopping
feed the cat
make dinner
build a rocket ship and fly away`,
sampleResult: `clean the house and go shopping and feed the cat and make dinner and build a rocket ship and fly away`,
requiredOptions: {
joinCharacter: 'and',
deleteBlankLines: true,
deleteTrailingSpaces: true
}
},
{
title: 'Comma Separated List',
description:
'This example joins a column of words into a comma separated list of words.',
sampleText: `computer
memory
processor
mouse
keyboard`,
sampleResult: `computer, memory, processor, mouse, keyboard`,
requiredOptions: {
joinCharacter: ',',
deleteBlankLines: false,
deleteTrailingSpaces: false
}
},
{
title: 'Vertical Word to Horizontal',
description:
'This example rotates words from a vertical position to horizontal. An empty separator is used for this purpose.',
sampleText: `T
e
x
t
a
b
u
l
o
u
s
!`,
sampleResult: `Textabulous!`,
requiredOptions: {
joinCharacter: '',
deleteBlankLines: false,
deleteTrailingSpaces: false
}
}
];
export default function JoinText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (optionsValues: typeof initialValues, input: any) => {
const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
};
function changeInputResult(input: string, result: string) {
setInput(input);
setResult(result);
const toolsElement = document.getElementById('tool');
if (toolsElement) {
toolsElement.scrollIntoView({ behavior: 'smooth' });
}
}
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Text Pieces'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Joined Text'} value={result} />}
/>
<ToolOptions
compute={compute}
getGroups={({ values, updateField }) => [
{
title: 'Text Merged Options',
component: (
<TextFieldWithDesc
placeholder={mergeOptions.placeholder}
value={values['joinCharacter']}
onOwnChange={(value) =>
updateField(mergeOptions.accessor, value)
}
description={mergeOptions.description}
/>
)
},
{
title: 'Blank Lines and Trailing Spaces',
component: blankTrailingOptions.map((option) => (
<CheckboxWithDesc
key={option.accessor}
title={option.title}
checked={!!values[option.accessor]}
onChange={(value) => updateField(option.accessor, value)}
description={option.description}
/>
))
}
]}
initialValues={initialValues}
input={input}
/>
<ToolInfo
title="What Is a Text Joiner?"
description="With this tool you can join parts of the text together. It takes a list of text values, separated by newlines, and merges them together. You can set the character that will be placed between the parts of the combined text. Also, you can ignore all empty lines and remove spaces and tabs at the end of all lines. Textabulous!"
/>
<Separator backgroundColor="#5581b5" margin="50px" />
<Examples
title="Text Joiner Examples"
subtitle="Click to try!"
exampleCards={exampleCards.map((card) => ({
...card,
changeInputResult
}))}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/text.png';
export const tool = defineTool('string', {
path: 'join',
name: 'Text Joiner',
image,
description:
"World's Simplest Text Tool World's simplest browser-based utility for joining text. Load your text in the input form on the left and you'll automatically get merged text on the right. Powerful, free, and fast. Load text get joined lines",
shortDescription: 'Quickly merge texts',
keywords: ['text', 'join'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,17 @@
export function mergeText(
text: string,
deleteBlankLines: boolean = true,
deleteTrailingSpaces: boolean = true,
joinCharacter: string = ''
): string {
let processedLines: string[] = text.split('\n');
if (deleteTrailingSpaces) {
processedLines = processedLines.map((line) => line.trimEnd());
}
if (deleteBlankLines) {
processedLines = processedLines.filter((line) => line.trim());
}
return processedLines.join(joinCharacter);
}

View File

@@ -0,0 +1,18 @@
import { expect, test } from '@playwright/test';
test.describe('JoinText Component', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/string/join');
});
test('should merge text pieces with specified join character', async ({
page
}) => {
// Input the text pieces
await page.getByTestId('text-input').fill('1\n2');
const result = await page.getByTestId('text-result').inputValue();
expect(result).toBe('12');
});
});

View File

@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { mergeText } from './service';
describe('mergeText', () => {
it('should merge lines with default settings (delete blank lines, delete trailing spaces, join with empty string)', () => {
const input = 'line1 \n \nline2\nline3 \n\nline4';
const expected = 'line1line2line3line4';
expect(mergeText(input)).toBe(expected);
});
it('should merge lines and preserve blank lines when deleteBlankLines is false', () => {
const input = 'line1 \n \nline2\nline3 \n\nline4';
const expected = 'line1line2line3line4';
expect(mergeText(input, false, true, '')).toBe(expected);
});
it('should merge lines and preserve trailing spaces when deleteTrailingSpaces is false', () => {
const input = 'line1 \n \nline2\nline3 \n\nline4';
const expected = 'line1 line2line3 line4';
expect(mergeText(input, true, false)).toBe(expected);
});
it('should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false', () => {
const input = 'line1 \n \nline2\nline3 \n\nline4';
const expected = 'line1 line2line3 line4';
expect(mergeText(input, false, false)).toBe(expected);
});
it('should merge lines with a specified joinCharacter', () => {
const input = 'line1 \n \nline2\nline3 \n\nline4';
const expected = 'line1 line2 line3 line4';
expect(mergeText(input, true, true, ' ')).toBe(expected);
});
it('should handle empty input', () => {
const input = '';
const expected = '';
expect(mergeText(input)).toBe(expected);
});
it('should handle input with only blank lines', () => {
const input = ' \n \n\n';
const expected = '';
expect(mergeText(input)).toBe(expected);
});
it('should handle input with only trailing spaces', () => {
const input = 'line1 \nline2 \nline3 ';
const expected = 'line1line2line3';
expect(mergeText(input)).toBe(expected);
});
it('should handle single line input', () => {
const input = 'single line';
const expected = 'single line';
expect(mergeText(input)).toBe(expected);
});
it('should join lines with new line character when joinCharacter is set to "\\n"', () => {
const input = 'line1 \n \nline2\nline3 \n\nline4';
const expected = 'line1\nline2\nline3\nline4';
expect(mergeText(input, true, true, '\n')).toBe(expected);
});
});

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Palindrome() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'Palindrome',
path: 'palindrome',
// image,
description: '',
shortDescription: '',
keywords: ['palindrome'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,60 @@
import { describe, expect } from 'vitest';
import { palindromeList } from './service';
describe('palindromeList', () => {
test('should return true for single character words', () => {
const input = 'a|b|c';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true|true|true');
});
test('should return false for non-palindromes', () => {
const input = 'hello|world';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('false|false');
});
test('should split using regex', () => {
const input = 'racecar,abba,hello';
const separator = ',';
const result = palindromeList('regex', input, separator);
expect(result).toBe('true,true,false');
});
test('should return empty string for empty input', () => {
const input = '';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('');
});
test('should split using custom separator', () => {
const input = 'racecar;abba;hello';
const separator = ';';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true;true;false');
});
test('should handle leading and trailing spaces', () => {
const input = ' racecar | abba | hello ';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true|true|false');
});
test('should handle multilines checking with trimming', () => {
const input = ' racecar \n abba \n hello ';
const separator = '\n';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true\ntrue\nfalse');
});
test('should handle empty strings in input', () => {
const input = 'racecar||hello';
const separator = '|';
const result = palindromeList('symbol', input, separator);
expect(result).toBe('true|true|false');
});
});

View File

@@ -0,0 +1,41 @@
export type SplitOperatorType = 'symbol' | 'regex';
function isPalindrome(word: string, left: number, right: number): boolean {
if (left >= right) return true;
if (word[left] !== word[right]) return false;
return isPalindrome(word, left + 1, right - 1);
}
// check each word of the input and add the palindrome status in an array
function checkPalindromes(array: string[]): boolean[] {
const status: boolean[] = [];
for (const word of array) {
const palindromeStatus = isPalindrome(word, 0, word.length - 1);
status.push(palindromeStatus);
}
return status;
}
export function palindromeList(
splitOperatorType: SplitOperatorType,
input: string,
separator: string // the splitting separator will be the joining separator for visual satisfaction
): string {
if (!input) return '';
let array: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(separator);
break;
case 'regex':
array = input.split(new RegExp(separator));
break;
}
// trim all items to focus on the word and not biasing the result due to spaces (leading and trailing)
array = array.map((item) => item.trim());
const statusArray = checkPalindromes(array);
return statusArray.map((status) => status.toString()).join(separator);
}

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function RandomizeCase() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'Randomize case',
path: 'randomize-case',
// image,
description: '',
shortDescription: '',
keywords: ['randomize', 'case'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import { randomizeCase } from './service';
describe('randomizeCase', () => {
it('should randomize the case of each character in the string', () => {
const input = 'hello world';
const result = randomizeCase(input);
// Ensure the output length is the same
expect(result).toHaveLength(input.length);
// Ensure each character in the input string appears in the result
for (let i = 0; i < input.length; i++) {
const inputChar = input[i];
const resultChar = result[i];
if (/[a-zA-Z]/.test(inputChar)) {
expect([inputChar.toLowerCase(), inputChar.toUpperCase()]).toContain(
resultChar
);
} else {
expect(inputChar).toBe(resultChar);
}
}
});
it('should handle an empty string', () => {
const input = '';
const result = randomizeCase(input);
expect(result).toBe('');
});
it('should handle a string with numbers and symbols', () => {
const input = '123 hello! @world';
const result = randomizeCase(input);
// Ensure the output length is the same
expect(result).toHaveLength(input.length);
// Ensure numbers and symbols remain unchanged
for (let i = 0; i < input.length; i++) {
const inputChar = input[i];
const resultChar = result[i];
if (!/[a-zA-Z]/.test(inputChar)) {
expect(inputChar).toBe(resultChar);
}
}
});
});

View File

@@ -0,0 +1,8 @@
export function randomizeCase(input: string): string {
return input
.split('')
.map((char) =>
Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()
)
.join('');
}

View File

@@ -0,0 +1,11 @@
import { Box } from '@mui/material';
import React from 'react';
import * as Yup from 'yup';
const initialValues = {};
const validationSchema = Yup.object({
// splitSeparator: Yup.string().required('The separator is required')
});
export default function Reverse() {
return <Box>Lorem ipsum</Box>;
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'Reverse',
path: 'reverse',
// image,
description: '',
shortDescription: '',
keywords: ['reverse'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import { stringReverser } from './service';
describe('stringReverser', () => {
it('should reverse a single-line string', () => {
const input = 'hello world';
const result = stringReverser(input, false, false, false);
expect(result).toBe('dlrow olleh');
});
it('should reverse each line in a multi-line string', () => {
const input = 'hello\nworld';
const result = stringReverser(input, true, false, false);
expect(result).toBe('olleh\ndlrow');
});
it('should remove empty items if emptyItems is true', () => {
const input = 'hello\n\nworld';
const result = stringReverser(input, true, true, false);
expect(result).toBe('olleh\ndlrow');
});
it('should trim each line if trim is true', () => {
const input = ' hello \n world ';
const result = stringReverser(input, true, false, true);
expect(result).toBe('olleh\ndlrow');
});
it('should handle empty input', () => {
const input = '';
const result = stringReverser(input, false, false, false);
expect(result).toBe('');
});
it('should handle a single line with emptyItems and trim', () => {
const input = ' hello world ';
const result = stringReverser(input, false, true, true);
expect(result).toBe('dlrow olleh');
});
it('should handle a single line with emptyItems and non trim', () => {
const input = ' hello world ';
const result = stringReverser(input, false, true, false);
expect(result).toBe(' dlrow olleh ');
});
it('should handle a multi line with emptyItems and non trim', () => {
const input = ' hello\n\n\n\nworld ';
const result = stringReverser(input, true, true, false);
expect(result).toBe('olleh \n dlrow');
});
});

View File

@@ -0,0 +1,30 @@
import { reverseString } from 'utils/string';
export function stringReverser(
input: string,
multiLine: boolean,
emptyItems: boolean,
trim: boolean
) {
let array: string[] = [];
let result: string[] = [];
// split the input in multiLine mode
if (multiLine) {
array = input.split('\n');
} else {
array.push(input);
}
// handle empty items
if (emptyItems) {
array = array.filter(Boolean);
}
// Handle trim
if (trim) {
array = array.map((line) => line.trim());
}
result = array.map((element) => reverseString(element));
return result.join('\n');
}

View File

@@ -0,0 +1,148 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { compute, SplitOperatorType } from './service';
import RadioWithTextField from '@components/options/RadioWithTextField';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
symbolValue: ' ',
regexValue: '/\\s+/',
lengthValue: '16',
chunksValue: '4',
outputSeparator: '\\n',
charBeforeChunk: '',
charAfterChunk: ''
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
{
title: 'Use a Symbol for Splitting',
description:
'Character that will be used to\n' +
'break text into parts.\n' +
'(Space by default.)',
type: 'symbol'
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description:
'Regular expression that will be\n' +
'used to break text into parts.\n' +
'(Multiple spaces by default.)'
},
{
title: 'Use Length for Splitting',
description:
'Number of symbols that will be\n' + 'put in each output chunk.',
type: 'length'
},
{
title: 'Use a Number of Chunks',
description: 'Number of chunks of equal\n' + 'length in the output.',
type: 'chunks'
}
];
const outputOptions: {
description: string;
accessor: keyof typeof initialValues;
}[] = [
{
description:
'Character that will be put\n' +
'between the split chunks.\n' +
'(It\'s newline "\\n" by default.)',
accessor: 'outputSeparator'
},
{
description: 'Character before each chunk',
accessor: 'charBeforeChunk'
},
{
description: 'Character after each chunk',
accessor: 'charAfterChunk'
}
];
export default function SplitText() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
const computeExternal = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
outputSeparator,
charBeforeChunk,
charAfterChunk,
chunksValue,
symbolValue,
regexValue,
lengthValue
} = optionsValues;
setResult(
compute(
splitSeparatorType,
input,
symbolValue,
regexValue,
Number(lengthValue),
Number(chunksValue),
charBeforeChunk,
charAfterChunk,
outputSeparator
)
);
};
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Text pieces'} value={result} />}
/>
<ToolOptions
compute={computeExternal}
getGroups={({ values, updateField }) => [
{
title: 'Split separator options',
component: splitOperators.map(({ title, description, type }) => (
<RadioWithTextField
key={type}
checked={type === values.splitSeparatorType}
title={title}
fieldName={'splitSeparatorType'}
description={description}
value={values[`${type}Value`]}
onRadioClick={() => updateField('splitSeparatorType', type)}
onTextChange={(val) => updateField(`${type}Value`, val)}
/>
))
},
{
title: 'Output separator options',
component: outputOptions.map((option) => (
<TextFieldWithDesc
key={option.accessor}
value={values[option.accessor]}
onOwnChange={(value) => updateField(option.accessor, value)}
description={option.description}
/>
))
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
import image from '@assets/text.png';
export const tool = defineTool('string', {
path: 'split',
name: 'Text splitter',
image,
description:
"World's simplest browser-based utility for splitting text. Load your text in the input form on the left and you'll automatically get pieces of this text on the right. Powerful, free, and fast. Load text get chunks.",
shortDescription: 'Quickly split a text',
keywords: ['text', 'split'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,65 @@
export type SplitOperatorType = 'symbol' | 'regex' | 'length' | 'chunks';
function splitTextByLength(text: string, length: number) {
if (length <= 0) throw new Error('Length must be a positive number');
const result: string[] = [];
for (let i = 0; i < text.length; i += length) {
result.push(text.slice(i, i + length));
}
return result;
}
function splitIntoChunks(text: string, numChunks: number) {
if (numChunks <= 0)
throw new Error('Number of chunks must be a positive number');
const totalLength = text.length;
if (totalLength < numChunks)
throw new Error(
'Text length must be at least as long as the number of chunks'
);
const chunkSize = Math.ceil(totalLength / numChunks); // Calculate the chunk size, rounding up to handle remainders
let result = [];
for (let i = 0; i < totalLength; i += chunkSize) {
result.push(text.slice(i, i + chunkSize));
}
// Ensure the result contains exactly numChunks, adjusting the last chunk if necessary
if (result.length > numChunks) {
result[numChunks - 1] = result.slice(numChunks - 1).join(''); // Merge any extra chunks into the last chunk
result = result.slice(0, numChunks); // Take only the first numChunks chunks
}
return result;
}
export function compute(
splitSeparatorType: SplitOperatorType,
input: string,
symbolValue: string,
regexValue: string,
lengthValue: number,
chunksValue: number,
charBeforeChunk: string,
charAfterChunk: string,
outputSeparator: string
) {
let splitText;
switch (splitSeparatorType) {
case 'symbol':
splitText = input.split(symbolValue);
break;
case 'regex':
splitText = input.split(new RegExp(regexValue));
break;
case 'length':
splitText = splitTextByLength(input, lengthValue);
break;
case 'chunks':
splitText = splitIntoChunks(input, chunksValue).map(
(chunk) => `${charBeforeChunk}${chunk}${charAfterChunk}`
);
}
return splitText.join(outputSeparator);
}

View File

@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import { compute } from './service';
describe('compute function', () => {
it('should split by symbol', () => {
const result = compute('symbol', 'hello world', ' ', '', 0, 0, '', '', ',');
expect(result).toBe('hello,world');
});
it('should split by regex', () => {
const result = compute(
'regex',
'hello1world2again',
'',
'\\d',
0,
0,
'',
'',
','
);
expect(result).toBe('hello,world,again');
});
it('should split by length', () => {
const result = compute('length', 'helloworld', '', '', 3, 0, '', '', ',');
expect(result).toBe('hel,low,orl,d');
});
it('should split into chunks', () => {
const result = compute(
'chunks',
'helloworldagain',
'',
'',
0,
3,
'[',
']',
','
);
expect(result).toBe('[hello],[world],[again]');
});
it('should handle empty input', () => {
const result = compute('symbol', '', ' ', '', 0, 0, '', '', ',');
expect(result).toBe('');
});
it('should handle length greater than text length', () => {
const result = compute('length', 'hi', '', '', 5, 0, '', '', ',');
expect(result).toBe('hi');
});
it('should handle chunks greater than text length', () => {
expect(() => {
compute('chunks', 'hi', '', '', 0, 5, '', '', ',');
}).toThrow('Text length must be at least as long as the number of chunks');
});
it('should handle invalid length', () => {
expect(() => {
compute('length', 'hello', '', '', -1, 0, '', '', ',');
}).toThrow('Length must be a positive number');
});
it('should handle invalid chunks', () => {
expect(() => {
compute('chunks', 'hello', '', '', 0, 0, '', '', ',');
}).toThrow('Number of chunks must be a positive number');
});
});

View File

@@ -0,0 +1,63 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions from '@components/options/ToolOptions';
import { compute } from './service';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
const initialValues = {
dotSymbol: '.',
dashSymbol: '-'
};
export default function ToMorse() {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
const computeOptions = (optionsValues: typeof initialValues, input: any) => {
const { dotSymbol, dashSymbol } = optionsValues;
setResult(compute(input, dotSymbol, dashSymbol));
};
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Morse code'} value={result} />}
/>
<ToolOptions
compute={computeOptions}
getGroups={({ values, updateField }) => [
{
title: 'Short Signal',
component: (
<TextFieldWithDesc
description={
'Symbol that will correspond to the dot in Morse code.'
}
value={values.dotSymbol}
onOwnChange={(val) => updateField('dotSymbol', val)}
/>
)
},
{
title: 'Long Signal',
component: (
<TextFieldWithDesc
description={
'Symbol that will correspond to the dash in Morse code.'
}
value={values.dashSymbol}
onOwnChange={(val) => updateField('dashSymbol', val)}
/>
)
}
]}
initialValues={initialValues}
input={input}
/>
</Box>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
// import image from '@assets/text.png';
export const tool = defineTool('string', {
name: 'String To morse',
path: 'to-morse',
// image,
description:
"World's simplest browser-based utility for converting text to Morse code. Load your text in the input form on the left and you'll instantly get Morse code in the output area. Powerful, free, and fast. Load text get Morse code.",
shortDescription: 'Quickly encode text to morse',
keywords: ['to', 'morse'],
component: lazy(() => import('./index'))
});

Some files were not shown because too many files have changed in this diff Show More