mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-25 17:09:33 +02:00
Merge remote-tracking branch 'origin/main' into string-join
# Conflicts: # src/pages/string/join/index.tsx
This commit is contained in:
@@ -12,21 +12,21 @@ import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { filterTools, getToolsByCategory, tools } from '../../tools';
|
||||
import { useState } from 'react';
|
||||
import { DefinedTool } from '../../tools/defineTool';
|
||||
import { DefinedTool } from '@tools/defineTool';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
const exampleTools: { label: string; url: string }[] = [
|
||||
{
|
||||
label: 'Create a transparent image',
|
||||
url: ''
|
||||
url: '/png/create-transparent'
|
||||
},
|
||||
{ label: 'Convert text to morse code', url: '' },
|
||||
{ label: 'Convert text to morse code', url: '/string/to-morse' },
|
||||
{ label: 'Change GIF speed', url: '' },
|
||||
{ label: 'Pick a random item', url: '' },
|
||||
{ label: 'Find and replace text', url: '' },
|
||||
{ label: 'Convert emoji to image', url: '' },
|
||||
{ label: 'Split a string', url: '/string/split' },
|
||||
{ label: 'Calculate number sum', url: '' },
|
||||
{ label: 'Calculate number sum', url: '/number/sum' },
|
||||
{ label: 'Pixelate an image', url: '' }
|
||||
];
|
||||
export default function Home() {
|
||||
|
@@ -1,11 +1,168 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import React, { useEffect, 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 { Formik, useFormikContext } from 'formik';
|
||||
import ColorSelector from '../../../../components/options/ColorSelector';
|
||||
import Color from 'color';
|
||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
|
||||
import ToolOptionGroups from '../../../../components/options/ToolOptionGroups';
|
||||
|
||||
const initialValues = {};
|
||||
const initialValues = {
|
||||
fromColor: 'white',
|
||||
toColor: 'black',
|
||||
similarity: '10'
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function ChangeColorsInPng() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const FormikListenerComponent = ({ input }: { input: File }) => {
|
||||
const { values } = useFormikContext<typeof initialValues>();
|
||||
const { fromColor, toColor, similarity } = values;
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
|
||||
const colorDistance = (
|
||||
c1: [number, number, number],
|
||||
c2: [number, number, number]
|
||||
) => {
|
||||
return Math.sqrt(
|
||||
Math.pow(c1[0] - c2[0], 2) +
|
||||
Math.pow(c1[1] - c2[1], 2) +
|
||||
Math.pow(c1[2] - c2[2], 2)
|
||||
);
|
||||
};
|
||||
const maxColorDistance = Math.sqrt(
|
||||
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
|
||||
);
|
||||
const similarityThreshold = (similarity / 100) * maxColorDistance;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const currentColor: [number, number, number] = [
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2]
|
||||
];
|
||||
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
|
||||
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));
|
||||
}, [input, fromColor, toColor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
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>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={() => {}}
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Box>
|
||||
{input && <FormikListenerComponent input={input} />}
|
||||
<ToolOptionGroups
|
||||
groups={[
|
||||
{
|
||||
title: 'From color and to color',
|
||||
component: (
|
||||
<Box>
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onChange={(val) => setFieldValue('fromColor', val)}
|
||||
description={'Replace this color (from color)'}
|
||||
/>
|
||||
<ColorSelector
|
||||
value={values.toColor}
|
||||
onChange={(val) => setFieldValue('toColor', val)}
|
||||
description={'With this color (to color)'}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onChange={(val) => setFieldValue('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>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Formik>
|
||||
</ToolOptions>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import image from '@assets/text.png';
|
||||
import image from '@assets/image.png';
|
||||
|
||||
export const tool = defineTool('image/png', {
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Change colors in png',
|
||||
path: '/change-colors-in-png',
|
||||
path: 'change-colors-in-png',
|
||||
image,
|
||||
description: '',
|
||||
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.",
|
||||
keywords: ['change', 'colors', 'in', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
156
src/pages/image/png/create-transparent/index.tsx
Normal file
156
src/pages/image/png/create-transparent/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useEffect, 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 { Formik, useFormikContext } from 'formik';
|
||||
import ColorSelector from '../../../../components/options/ColorSelector';
|
||||
import Color from 'color';
|
||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '../../../../components/ToolInputAndResult';
|
||||
import ToolOptionGroups from '../../../../components/options/ToolOptionGroups';
|
||||
|
||||
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 FormikListenerComponent = ({ input }: { input: File }) => {
|
||||
const { values } = useFormikContext<typeof initialValues>();
|
||||
const { fromColor, similarity } = values;
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
|
||||
const colorDistance = (
|
||||
c1: [number, number, number],
|
||||
c2: [number, number, number]
|
||||
) => {
|
||||
return Math.sqrt(
|
||||
Math.pow(c1[0] - c2[0], 2) +
|
||||
Math.pow(c1[1] - c2[1], 2) +
|
||||
Math.pow(c1[2] - c2[2], 2)
|
||||
);
|
||||
};
|
||||
const maxColorDistance = Math.sqrt(
|
||||
Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2)
|
||||
);
|
||||
const similarityThreshold = (similarity / 100) * maxColorDistance;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const currentColor: [number, number, number] = [
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2]
|
||||
];
|
||||
if (colorDistance(currentColor, fromColor) <= similarityThreshold) {
|
||||
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));
|
||||
}, [input, fromColor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
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>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={() => {}}
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Box>
|
||||
{input && <FormikListenerComponent input={input} />}
|
||||
<ToolOptionGroups
|
||||
groups={[
|
||||
{
|
||||
title: 'From color and similarity',
|
||||
component: (
|
||||
<Box>
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onChange={(val) => setFieldValue('fromColor', val)}
|
||||
description={'Replace this color (from color)'}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onChange={(val) => setFieldValue('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>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Formik>
|
||||
</ToolOptions>
|
||||
</Box>
|
||||
);
|
||||
}
|
13
src/pages/image/png/create-transparent/meta.ts
Normal file
13
src/pages/image/png/create-transparent/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
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'))
|
||||
});
|
@@ -1,3 +1,4 @@
|
||||
import { tool as pngCreateTransparent } from './create-transparent/meta';
|
||||
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
|
||||
|
||||
export const pngTools = [changeColorsInPng];
|
||||
export const pngTools = [changeColorsInPng, pngCreateTransparent];
|
||||
|
3
src/pages/number/index.ts
Normal file
3
src/pages/number/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { tool as numberSum } from './sum/meta';
|
||||
|
||||
export const numberTools = [numberSum];
|
153
src/pages/number/sum/index.tsx
Normal file
153
src/pages/number/sum/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import ToolTextInput from '../../../components/input/ToolTextInput';
|
||||
import ToolTextResult from '../../../components/result/ToolTextResult';
|
||||
import { Formik, useFormikContext } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import ToolOptions from '../../../components/options/ToolOptions';
|
||||
import { compute, NumberExtractionType } from './service';
|
||||
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
|
||||
import RadioWithTextField from '../../../components/options/RadioWithTextField';
|
||||
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
|
||||
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>('');
|
||||
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const FormikListenerComponent = () => {
|
||||
const { values } = useFormikContext<typeof initialValues>();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const { extractionType, printRunningSum, separator } = values;
|
||||
|
||||
setResult(compute(input, extractionType, printRunningSum, separator));
|
||||
} catch (exception: unknown) {
|
||||
if (exception instanceof Error)
|
||||
showSnackBar(exception.message, 'error');
|
||||
}
|
||||
}, [values, input]);
|
||||
|
||||
return null; // This component doesn't render anything
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Total'} value={result} />}
|
||||
/>
|
||||
<ToolOptions>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={() => {}}
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Stack direction={'row'} spacing={2}>
|
||||
<FormikListenerComponent />
|
||||
<ToolOptionGroups
|
||||
groups={[
|
||||
{
|
||||
title: 'Number extraction',
|
||||
component: extractionTypes.map(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
type,
|
||||
withTextField,
|
||||
textValueAccessor
|
||||
}) =>
|
||||
withTextField ? (
|
||||
<RadioWithTextField
|
||||
key={type}
|
||||
radioValue={type}
|
||||
title={title}
|
||||
fieldName={'extractionType'}
|
||||
description={description}
|
||||
value={
|
||||
textValueAccessor
|
||||
? values[textValueAccessor].toString()
|
||||
: ''
|
||||
}
|
||||
onRadioChange={(type) =>
|
||||
setFieldValue('extractionType', type)
|
||||
}
|
||||
onTextChange={(val) =>
|
||||
setFieldValue(textValueAccessor ?? '', val)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<SimpleRadio
|
||||
key={title}
|
||||
onChange={() =>
|
||||
setFieldValue('extractionType', type)
|
||||
}
|
||||
fieldName={'extractionType'}
|
||||
value={values.extractionType}
|
||||
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) =>
|
||||
setFieldValue('printRunningSum', value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Formik>
|
||||
</ToolOptions>
|
||||
</Box>
|
||||
);
|
||||
}
|
13
src/pages/number/sum/meta.ts
Normal file
13
src/pages/number/sum/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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.',
|
||||
keywords: ['sum'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
37
src/pages/number/sum/service.ts
Normal file
37
src/pages/number/sum/service.ts
Normal 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();
|
||||
};
|
64
src/pages/number/sum/sum.service.test.ts
Normal file
64
src/pages/number/sum/sum.service.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } 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');
|
||||
});
|
||||
});
|
@@ -1,4 +1,5 @@
|
||||
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];
|
||||
export const stringTools = [stringSplit, stringJoin, stringToMorse];
|
||||
|
@@ -9,6 +9,8 @@ import { mergeText } from './service';
|
||||
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
|
||||
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
|
||||
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
|
||||
import ToolInputAndResult from '../../../components/ToolInputAndResult';
|
||||
|
||||
import Info from './Info';
|
||||
import Separator from '../../../tools/Separator';
|
||||
@@ -56,12 +58,12 @@ 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
|
||||
sampleText: `clean the house
|
||||
|
||||
go shopping
|
||||
go shopping
|
||||
feed the cat
|
||||
|
||||
make dinner
|
||||
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: {
|
||||
@@ -177,18 +179,16 @@ export default function JoinText() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Text Pieces'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ToolTextResult title={'Joined Text'} value={result} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
result={<ToolTextResult title={'Joined Text'} value={result} />}
|
||||
/>
|
||||
<ToolOptions>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
@@ -198,31 +198,37 @@ export default function JoinText() {
|
||||
{({ setFieldValue, values }) => (
|
||||
<Stack direction={'row'} spacing={2}>
|
||||
<FormikListenerComponent input={input} />
|
||||
<Box>
|
||||
<Typography fontSize={22}>Text Merged Options</Typography>
|
||||
<TextFieldWithDesc
|
||||
placeholder={mergeOptions.placeholder}
|
||||
value={values['joinCharacter']}
|
||||
onChange={(value) =>
|
||||
setFieldValue(mergeOptions.accessor, value)
|
||||
<ToolOptionGroups
|
||||
groups={[
|
||||
{
|
||||
title: 'Text Merged Options',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
placeholder={mergeOptions.placeholder}
|
||||
value={values['joinCharacter']}
|
||||
onChange={(value) =>
|
||||
setFieldValue(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) =>
|
||||
setFieldValue(option.accessor, value)
|
||||
}
|
||||
description={option.description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
description={mergeOptions.description}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography fontSize={22}>
|
||||
Blank Lines and Trailing Spaces
|
||||
</Typography>
|
||||
{blankTrailingOptions.map((option, index) => (
|
||||
<CheckboxWithDesc
|
||||
key={index}
|
||||
title={option.title}
|
||||
checked={!!values[option.accessor]}
|
||||
onChange={(value) => setFieldValue(option.accessor, value)}
|
||||
description={option.description}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Formik>
|
||||
|
@@ -5,23 +5,13 @@ export function mergeText(
|
||||
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() !== '');
|
||||
} else {
|
||||
processedLines = processedLines.map((line) =>
|
||||
line.trim() === '' ? line + '\r\n\n' : line
|
||||
);
|
||||
processedLines = processedLines.filter((line) => line.trim());
|
||||
}
|
||||
|
||||
return processedLines.join(joinCharacter);
|
||||
}
|
||||
|
||||
// Example text to use
|
||||
`This is a line with trailing spaces
|
||||
Another line with trailing spaces
|
||||
|
||||
Final line without trailing spaces`;
|
||||
|
@@ -1,16 +1,17 @@
|
||||
import { Box, Stack, TextField } from '@mui/material';
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import ToolTextInput from '../../../components/input/ToolTextInput';
|
||||
import ToolTextResult from '../../../components/result/ToolTextResult';
|
||||
import { Field, Formik, FormikProps, useFormikContext } from 'formik';
|
||||
import { Formik, useFormikContext } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import ToolOptions from '../../../components/options/ToolOptions';
|
||||
import { compute, SplitOperatorType } from './service';
|
||||
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
|
||||
import RadioWithTextField from '../../../components/options/RadioWithTextField';
|
||||
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
|
||||
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
|
||||
import ToolInputAndResult from '../../../components/ToolInputAndResult';
|
||||
|
||||
const initialValues = {
|
||||
splitSeparatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -126,14 +127,10 @@ export default function SplitText() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<ToolTextInput value={input} onChange={setInput} />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ToolTextResult title={'Text pieces'} value={result} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Text pieces'} value={result} />}
|
||||
/>
|
||||
<ToolOptions>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
@@ -143,34 +140,44 @@ export default function SplitText() {
|
||||
{({ setFieldValue, values }) => (
|
||||
<Stack direction={'row'} spacing={2}>
|
||||
<FormikListenerComponent />
|
||||
<Box>
|
||||
<Typography fontSize={22}>Split separator options</Typography>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<RadioWithTextField
|
||||
key={type}
|
||||
type={type}
|
||||
title={title}
|
||||
fieldName={'splitSeparatorType'}
|
||||
description={description}
|
||||
value={values[`${type}Value`]}
|
||||
onTypeChange={(type) =>
|
||||
setFieldValue('splitSeparatorType', type)
|
||||
}
|
||||
onTextChange={(val) => setFieldValue(`${type}Value`, val)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography fontSize={22}>Output separator options</Typography>
|
||||
{outputOptions.map((option) => (
|
||||
<TextFieldWithDesc
|
||||
key={option.accessor}
|
||||
value={values[option.accessor]}
|
||||
onChange={(value) => setFieldValue(option.accessor, value)}
|
||||
description={option.description}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<ToolOptionGroups
|
||||
groups={[
|
||||
{
|
||||
title: 'Split separator options',
|
||||
component: splitOperators.map(
|
||||
({ title, description, type }) => (
|
||||
<RadioWithTextField
|
||||
key={type}
|
||||
radioValue={type}
|
||||
title={title}
|
||||
fieldName={'splitSeparatorType'}
|
||||
description={description}
|
||||
value={values[`${type}Value`]}
|
||||
onRadioChange={(type) =>
|
||||
setFieldValue('splitSeparatorType', type)
|
||||
}
|
||||
onTextChange={(val) =>
|
||||
setFieldValue(`${type}Value`, val)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output separator options',
|
||||
component: outputOptions.map((option) => (
|
||||
<TextFieldWithDesc
|
||||
key={option.accessor}
|
||||
value={values[option.accessor]}
|
||||
onChange={(value) =>
|
||||
setFieldValue(option.accessor, value)
|
||||
}
|
||||
description={option.description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Formik>
|
||||
|
95
src/pages/string/to-morse/index.tsx
Normal file
95
src/pages/string/to-morse/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import ToolTextInput from '../../../components/input/ToolTextInput';
|
||||
import ToolTextResult from '../../../components/result/ToolTextResult';
|
||||
import { Formik, useFormikContext } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import ToolOptions from '../../../components/options/ToolOptions';
|
||||
import { compute } from './service';
|
||||
import { CustomSnackBarContext } from '../../../contexts/CustomSnackBarContext';
|
||||
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
|
||||
import ToolOptionGroups from '../../../components/options/ToolOptionGroups';
|
||||
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 { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const FormikListenerComponent = () => {
|
||||
const { values } = useFormikContext<typeof initialValues>();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const { dotSymbol, dashSymbol } = values;
|
||||
|
||||
setResult(compute(input, dotSymbol, dashSymbol));
|
||||
} catch (exception: unknown) {
|
||||
if (exception instanceof Error)
|
||||
showSnackBar(exception.message, 'error');
|
||||
}
|
||||
}, [values, input]);
|
||||
|
||||
return null; // This component doesn't render anything
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Morse code'} value={result} />}
|
||||
/>
|
||||
<ToolOptions>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={() => {}}
|
||||
>
|
||||
{({ setFieldValue, values }) => (
|
||||
<Stack direction={'row'} spacing={2}>
|
||||
<FormikListenerComponent />
|
||||
<ToolOptionGroups
|
||||
groups={[
|
||||
{
|
||||
title: 'Short Signal',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Symbol that will correspond to the dot in Morse code.'
|
||||
}
|
||||
value={values.dotSymbol}
|
||||
onChange={(val) => setFieldValue('dotSymbol', val)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Long Signal',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Symbol that will correspond to the dash in Morse code.'
|
||||
}
|
||||
value={values.dashSymbol}
|
||||
onChange={(val) => setFieldValue('dashSymbol', val)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Formik>
|
||||
</ToolOptions>
|
||||
</Box>
|
||||
);
|
||||
}
|
13
src/pages/string/to-morse/meta.ts
Normal file
13
src/pages/string/to-morse/meta.ts
Normal 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: '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.",
|
||||
keywords: ['to', 'morse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
9
src/pages/string/to-morse/service.ts
Normal file
9
src/pages/string/to-morse/service.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { encode } from 'morsee';
|
||||
|
||||
export const compute = (
|
||||
input: string,
|
||||
dotSymbol: string,
|
||||
dashSymbol: string
|
||||
): string => {
|
||||
return encode(input).replaceAll('.', dotSymbol).replaceAll('-', dashSymbol);
|
||||
};
|
50
src/pages/string/to-morse/to-morse.service.test.ts
Normal file
50
src/pages/string/to-morse/to-morse.service.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { compute } from './service';
|
||||
|
||||
describe('compute function', () => {
|
||||
it('should replace dots and dashes with specified symbols', () => {
|
||||
const input = 'test';
|
||||
const dotSymbol = '*';
|
||||
const dashSymbol = '#';
|
||||
const result = compute(input, dotSymbol, dashSymbol);
|
||||
const expected = '# * *** #';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return an empty string for empty input', () => {
|
||||
const input = '';
|
||||
const dotSymbol = '*';
|
||||
const dashSymbol = '#';
|
||||
const result = compute(input, dotSymbol, dashSymbol);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
// Test case 3: Special characters handling
|
||||
it('should handle input with special characters', () => {
|
||||
const input = 'hello, world!';
|
||||
const dotSymbol = '*';
|
||||
const dashSymbol = '#';
|
||||
const result = compute(input, dotSymbol, dashSymbol);
|
||||
const expected =
|
||||
'**** * *#** *#** ### ##**## / *## ### *#* *#** #** #*#*##';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should work with different symbols for dots and dashes', () => {
|
||||
const input = 'morse';
|
||||
const dotSymbol = '!';
|
||||
const dashSymbol = '@';
|
||||
const result = compute(input, dotSymbol, dashSymbol);
|
||||
const expected = '@@ @@@ !@! !!! !';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle numeric input correctly', () => {
|
||||
const input = '12345';
|
||||
const dotSymbol = '*';
|
||||
const dashSymbol = '#';
|
||||
const result = compute(input, dotSymbol, dashSymbol);
|
||||
const expected = '*#### **### ***## ****# *****'; // This depends on how "12345" is encoded in morse code
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user