Merge remote-tracking branch 'origin/main' into truncate

# Conflicts:
#	src/pages/tools/string/index.ts
This commit is contained in:
Ibrahima G. Coulibaly
2025-03-07 22:13:09 +00:00
201 changed files with 4915 additions and 2145 deletions

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',
icon: '',
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',
icon: '',
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,30 @@
import { tool as stringRemoveDuplicateLines } from './remove-duplicate-lines/meta';
import { tool as stringRotate } from './rotate/meta';
import { tool as stringQuote } from './quote/meta';
import { tool as stringRot13 } from './rot13/meta';
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';
import { tool as stringReplace } from './text-replacer/meta';
import { tool as stringRepeat } from './repeat/meta';
export const stringTools = [
stringSplit,
stringJoin,
stringRemoveDuplicateLines,
stringToMorse,
stringReplace,
stringRepeat
// stringReverse,
// stringRandomizeCase,
// stringUppercase,
// stringExtractSubstring,
// stringCreatePalindrome,
// stringPalindrome
];

View File

@@ -0,0 +1,186 @@
import { Box } from '@mui/material';
import React, { useRef, useState } from 'react';
import * as Yup from 'yup';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions, { GetGroupsType } 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 ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { FormikProps } from 'formik';
import { ToolComponentProps } from '@tools/defineTool';
const initialValues = {
joinCharacter: '',
deleteBlank: true,
deleteTrailing: true
};
type InitialValuesType = typeof initialValues;
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 InitialValuesType
};
const blankTrailingOptions: {
title: string;
description: string;
accessor: keyof InitialValuesType;
}[] = [
{
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: CardExampleType<InitialValuesType>[] = [
{
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`,
sampleOptions: {
joinCharacter: 'and',
deleteBlank: true,
deleteTrailing: 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`,
sampleOptions: {
joinCharacter: ',',
deleteBlank: false,
deleteTrailing: 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!`,
sampleOptions: {
joinCharacter: '',
deleteBlank: false,
deleteTrailing: false
}
}
];
export default function JoinText({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const formRef = useRef<FormikProps<InitialValuesType>>(null);
const compute = (optionsValues: InitialValuesType, input: any) => {
const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
};
const getGroups: GetGroupsType<InitialValuesType> = ({
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}
/>
))
}
];
return (
<Box>
<ToolInputAndResult
input={
<ToolTextInput
title={'Text Pieces'}
value={input}
onChange={setInput}
/>
}
result={<ToolTextResult title={'Joined Text'} value={result} />}
/>
<ToolOptions
formRef={formRef}
compute={compute}
getGroups={getGroups}
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" />
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
path: 'join',
name: 'Text Joiner',
icon: 'tabler:arrows-join',
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',
icon: '',
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',
icon: '',
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,260 @@
import { Box } from '@mui/material';
import React, { useRef, useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
import SimpleRadio from '@components/options/SimpleRadio';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import ToolInputAndResult from '@components/ToolInputAndResult';
import ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
import { FormikProps } from 'formik';
import removeDuplicateLines, {
DuplicateRemovalMode,
DuplicateRemoverOptions,
NewlineOption
} from './service';
// Initial values for our form
const initialValues: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
// Operation mode options
const operationModes = [
{
title: 'Remove All Duplicate Lines',
description:
'If this option is selected, then all repeated lines across entire text are removed, starting from the second occurrence.',
value: 'all' as DuplicateRemovalMode
},
{
title: 'Remove Consecutive Duplicate Lines',
description:
'If this option is selected, then only consecutive repeated lines are removed.',
value: 'consecutive' as DuplicateRemovalMode
},
{
title: 'Leave Absolutely Unique Text Lines',
description:
'If this option is selected, then all lines that appear more than once are removed.',
value: 'unique' as DuplicateRemovalMode
}
];
// Newlines options
const newlineOptions = [
{
title: 'Preserve All Newlines',
description: 'Leave all empty lines in the output.',
value: 'preserve' as NewlineOption
},
{
title: 'Filter All Newlines',
description: 'Process newlines as regular lines.',
value: 'filter' as NewlineOption
},
{
title: 'Delete All Newlines',
description: 'Before filtering uniques, remove all newlines.',
value: 'delete' as NewlineOption
}
];
// Example cards for demonstration
const exampleCards: CardExampleType<typeof initialValues>[] = [
{
title: 'Remove Duplicate Items from List',
description:
'Removes duplicate items from a shopping list, keeping only the first occurrence of each item.',
sampleText: `Apples
Bananas
Milk
Eggs
Bread
Milk
Cheese
Apples
Yogurt`,
sampleResult: `Apples
Bananas
Milk
Eggs
Bread
Cheese
Yogurt`,
sampleOptions: {
...initialValues,
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
}
},
{
title: 'Clean Consecutive Duplicates',
description:
'Removes consecutive duplicates from log entries, which often happen when a system repeatedly logs the same error.',
sampleText: `[INFO] Application started
[ERROR] Connection failed
[ERROR] Connection failed
[ERROR] Connection failed
[INFO] Retrying connection
[ERROR] Authentication error
[ERROR] Authentication error
[INFO] Connection established`,
sampleResult: `[INFO] Application started
[ERROR] Connection failed
[INFO] Retrying connection
[ERROR] Authentication error
[INFO] Connection established`,
sampleOptions: {
...initialValues,
mode: 'consecutive',
newlines: 'filter',
sortLines: false,
trimTextLines: false
}
},
{
title: 'Extract Unique Entries Only',
description:
'Filters a list to keep only entries that appear exactly once, removing any duplicated items entirely.',
sampleText: `Red
Blue
Green
Blue
Yellow
Purple
Red
Orange`,
sampleResult: `Green
Yellow
Purple
Orange`,
sampleOptions: {
...initialValues,
mode: 'unique',
newlines: 'filter',
sortLines: false,
trimTextLines: false
}
},
{
title: 'Sort and Clean Data',
description:
'Removes duplicate items from a list, trims whitespace, and sorts the results alphabetically.',
sampleText: ` Apple
Banana
Cherry
Apple
Banana
Dragonfruit
Elderberry `,
sampleResult: `Apple
Banana
Cherry
Dragonfruit
Elderberry`,
sampleOptions: {
...initialValues,
mode: 'all',
newlines: 'filter',
sortLines: true,
trimTextLines: true
}
}
];
export default function RemoveDuplicateLines({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const formRef = useRef<FormikProps<typeof initialValues>>(null);
const computeExternal = (
optionsValues: typeof initialValues,
inputText: string
) => {
setResult(removeDuplicateLines(inputText, optionsValues));
};
const getGroups: GetGroupsType<typeof initialValues> = ({
values,
updateField
}) => [
{
title: 'Operation Mode',
component: operationModes.map(({ title, description, value }) => (
<SimpleRadio
key={value}
checked={value === values.mode}
title={title}
description={description}
onClick={() => updateField('mode', value)}
/>
))
},
{
title: 'Newlines, Tabs and Spaces',
component: [
...newlineOptions.map(({ title, description, value }) => (
<SimpleRadio
key={value}
checked={value === values.newlines}
title={title}
description={description}
onClick={() => updateField('newlines', value)}
/>
)),
<CheckboxWithDesc
key="trimTextLines"
checked={values.trimTextLines}
title="Trim Text Lines"
description="Before filtering uniques, remove tabs and spaces from the beginning and end of all lines."
onChange={(checked) => updateField('trimTextLines', checked)}
/>
]
},
{
title: 'Sort Lines',
component: [
<CheckboxWithDesc
key="sortLines"
checked={values.sortLines}
title="Sort the Output Lines"
description="After removing the duplicates, sort the unique lines."
onChange={(checked) => updateField('sortLines', checked)}
/>
]
}
];
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={
<ToolTextResult title={'Text without duplicates'} value={result} />
}
/>
<ToolOptions
compute={computeExternal}
getGroups={getGroups}
initialValues={initialValues}
input={input}
/>
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</Box>
);
}

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Remove duplicate lines',
path: 'remove-duplicate-lines',
icon: 'pepicons-print:duplicate-off',
description:
"Load your text in the input form on the left and you'll instantly get text with no duplicate lines in the output area. Powerful, free, and fast. Load text lines get unique text lines",
shortDescription: 'Quickly delete all repeated lines from text',
keywords: ['remove', 'duplicate', 'lines'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,200 @@
import { describe, expect, it } from 'vitest';
import removeDuplicateLines, { DuplicateRemoverOptions } from './service';
describe('removeDuplicateLines function', () => {
// Test for 'all' duplicate removal mode
describe('mode: all', () => {
it('should remove all duplicates keeping first occurrence', () => {
const input = 'line1\nline2\nline1\nline3\nline2';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2\nline3');
});
it('should handle case-sensitive duplicates correctly', () => {
const input = 'Line1\nline1\nLine2\nline2';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('Line1\nline1\nLine2\nline2');
});
});
// Test for 'consecutive' duplicate removal mode
describe('mode: consecutive', () => {
it('should remove only consecutive duplicates', () => {
const input = 'line1\nline1\nline2\nline3\nline3\nline1';
const options: DuplicateRemoverOptions = {
mode: 'consecutive',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2\nline3\nline1');
});
});
// Test for 'unique' duplicate removal mode
describe('mode: unique', () => {
it('should keep only lines that appear exactly once', () => {
const input = 'line1\nline2\nline1\nline3\nline4\nline4';
const options: DuplicateRemoverOptions = {
mode: 'unique',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line2\nline3');
});
});
// Test for newlines handling
describe('newlines option', () => {
it('should filter newlines when newlines is set to filter', () => {
const input = 'line1\n\nline2\n\n\nline3';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\n\nline2\nline3');
});
it('should delete newlines when newlines is set to delete', () => {
const input = 'line1\n\nline2\n\n\nline3';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'delete',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2\nline3');
});
it('should preserve newlines when newlines is set to preserve', () => {
const input = 'line1\n\nline2\n\nline2\nline3';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'preserve',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
// This test needs careful consideration of the expected behavior
expect(result).not.toContain('line2\nline2');
expect(result).toContain('line1');
expect(result).toContain('line2');
expect(result).toContain('line3');
});
});
// Test for sorting
describe('sortLines option', () => {
it('should sort lines when sortLines is true', () => {
const input = 'line3\nline1\nline2';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: true,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2\nline3');
});
});
// Test for trimming
describe('trimTextLines option', () => {
it('should trim lines when trimTextLines is true', () => {
const input = ' line1 \n line2 \nline3';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: true
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2\nline3');
});
it('should consider trimmed lines as duplicates', () => {
const input = ' line1 \nline1\n line2\nline2 ';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: true
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2');
});
});
// Combined scenarios
describe('combined options', () => {
it('should handle all options together correctly', () => {
const input = ' line3 \nline1\n\nline3\nline2\nline1';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'delete',
sortLines: true,
trimTextLines: true
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('line1\nline2\nline3');
});
});
// Edge cases
describe('edge cases', () => {
it('should handle empty input', () => {
const input = '';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('');
});
it('should handle input with only newlines', () => {
const input = '\n\n\n';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: false
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('');
});
it('should handle input with only whitespace', () => {
const input = ' \n \n ';
const options: DuplicateRemoverOptions = {
mode: 'all',
newlines: 'filter',
sortLines: false,
trimTextLines: true
};
const result = removeDuplicateLines(input, options);
expect(result).toBe('');
});
});
});

View File

@@ -0,0 +1,88 @@
export type NewlineOption = 'preserve' | 'filter' | 'delete';
export type DuplicateRemovalMode = 'all' | 'consecutive' | 'unique';
export interface DuplicateRemoverOptions {
mode: DuplicateRemovalMode;
newlines: NewlineOption;
sortLines: boolean;
trimTextLines: boolean;
}
/**
* Removes duplicate lines from text based on specified options
* @param text The input text to process
* @param options Configuration options for text processing
* @returns Processed text with duplicates removed according to options
*/
export default function removeDuplicateLines(
text: string,
options: DuplicateRemoverOptions
): string {
// Split the text into individual lines
let lines = text.split('\n');
// Process newlines based on option
if (options.newlines === 'delete') {
// Remove all empty lines
lines = lines.filter((line) => line.trim() !== '');
}
// Trim lines if option is selected
if (options.trimTextLines) {
lines = lines.map((line) => line.trim());
}
// Remove duplicates based on mode
let processedLines: string[] = [];
if (options.mode === 'all') {
// Remove all duplicates, keeping only first occurrence
const seen = new Set<string>();
processedLines = lines.filter((line) => {
if (seen.has(line)) {
return false;
}
seen.add(line);
return true;
});
} else if (options.mode === 'consecutive') {
// Remove only consecutive duplicates
processedLines = lines.filter((line, index, arr) => {
return index === 0 || line !== arr[index - 1];
});
} else if (options.mode === 'unique') {
// Leave only absolutely unique lines
const lineCount = new Map<string, number>();
lines.forEach((line) => {
lineCount.set(line, (lineCount.get(line) || 0) + 1);
});
processedLines = lines.filter((line) => lineCount.get(line) === 1);
}
// Sort lines if option is selected
if (options.sortLines) {
processedLines.sort();
}
// Process newlines for output
if (options.newlines === 'filter') {
// Process newlines as regular lines (already done by default)
} else if (options.newlines === 'preserve') {
// Make sure empty lines are preserved in the output
processedLines = text.split('\n').map((line) => {
if (line.trim() === '') return line;
return processedLines.includes(line) ? line : '';
});
}
return processedLines.join('\n');
}
// Example usage:
// const result = removeDuplicateLines(inputText, {
// mode: 'all',
// newlines: 'filter',
// sortLines: false,
// trimTextLines: true
// });

View File

@@ -0,0 +1,114 @@
import { Box } from '@mui/material';
import { useState } from 'react';
import ToolTextResult from '@components/result/ToolTextResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import { repeatText } from './service';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import ToolTextInput from '@components/input/ToolTextInput';
import { initialValues, InitialValuesType } from './initialValues';
import ToolContent from '@components/ToolContent';
import { CardExampleType } from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Repeat word five times',
description: 'Repeats "Hello!" five times without any delimiter.',
sampleText: 'Hello! ',
sampleResult: 'Hello! Hello! Hello! Hello! Hello! ',
sampleOptions: {
textToRepeat: 'Hello! ',
repeatAmount: '5',
delimiter: ''
}
},
{
title: 'Repeat phrase with comma',
description:
'Repeats "Good job" three times, separated by commas and spaces.',
sampleText: 'Good job',
sampleResult: 'Good job, Good job, Good job',
sampleOptions: {
textToRepeat: 'Good job',
repeatAmount: '3',
delimiter: ', '
}
},
{
title: 'Repeat number with space',
description: 'Repeats the number "42" four times, separated by spaces.',
sampleText: '42',
sampleResult: '42 42 42 42',
sampleOptions: {
textToRepeat: '42',
repeatAmount: '4',
delimiter: ' '
}
}
];
export default function Replacer({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
function compute(optionsValues: InitialValuesType, input: string) {
setResult(repeatText(optionsValues, input));
}
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Text Repetitions',
component: (
<Box>
<TextFieldWithDesc
description={'Number of repetitions.'}
placeholder="Number"
value={values.repeatAmount}
onOwnChange={(val) => updateField('repeatAmount', val)}
type={'number'}
/>
</Box>
)
},
{
title: 'Repetitions Delimiter',
component: (
<Box>
<TextFieldWithDesc
description={'Delimiter for output copies.'}
placeholder="Delimiter"
value={values.delimiter}
onOwnChange={(val) => updateField('delimiter', val)}
type={'text'}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
initialValues={initialValues}
getGroups={getGroups}
compute={compute}
input={input}
setInput={setInput}
inputComponent={
<ToolTextInput title={'Input text'} value={input} onChange={setInput} />
}
resultComponent={
<ToolTextResult title={'Repeated text'} value={result} />
}
toolInfo={{
title: 'Repeat text',
description:
'This tool allows you to repeat a given text multiple times with an optional separator.'
}}
exampleCards={exampleCards}
/>
);
}

View File

@@ -0,0 +1,11 @@
export type InitialValuesType = {
textToRepeat: string;
repeatAmount: string;
delimiter: string;
};
export const initialValues: InitialValuesType = {
textToRepeat: '',
repeatAmount: '5',
delimiter: ''
};

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Repeat text',
path: 'repeat',
shortDescription: 'Repeat text multiple times',
icon: 'material-symbols-light:replay',
description:
'This tool allows you to repeat a given text multiple times with an optional separator.',
keywords: ['text', 'repeat'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest';
import { repeatText } from './service';
import { initialValues } from './initialValues';
describe('repeatText function', () => {
it('should repeat the letter correctly', () => {
const text = 'i';
const repeatAmount = '5';
const result = repeatText({ ...initialValues, repeatAmount }, text);
expect(result).toBe('iiiii');
});
it('should repeat the word correctly', () => {
const text = 'hello';
const repeatAmount = '3';
const result = repeatText({ ...initialValues, repeatAmount }, text);
expect(result).toBe('hellohellohello');
});
it('should repeat the word with a space delimiter correctly', () => {
const text = 'word';
const repeatAmount = '3';
const delimiter = ' ';
const result = repeatText(
{ ...initialValues, repeatAmount, delimiter },
text
);
expect(result).toBe('word word word');
});
it('should repeat the word with a space and a comma delimiter correctly', () => {
const text = 'test';
const repeatAmount = '3';
const delimiter = ', ';
const result = repeatText(
{ ...initialValues, repeatAmount, delimiter },
text
);
expect(result).toBe('test, test, test');
});
it('Should not repeat text if repeatAmount is zero', () => {
const text = 'something';
const repeatAmount = '0';
const result = repeatText({ ...initialValues, repeatAmount }, text);
expect(result).toBe('');
});
it('Should not repeat text if repeatAmount is not entered', () => {
const text = 'something';
const repeatAmount = '';
const result = repeatText({ ...initialValues, repeatAmount }, text);
expect(result).toBe('');
});
});

View File

@@ -0,0 +1,9 @@
import { InitialValuesType } from './initialValues';
export function repeatText(options: InitialValuesType, text: string) {
const { repeatAmount, delimiter } = options;
const parsedAmount = parseInt(repeatAmount) || 0;
return Array(parsedAmount).fill(text).join(delimiter);
}

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',
icon: '',
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,218 @@
import { Box } from '@mui/material';
import React, { useRef, useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import ToolOptions, { GetGroupsType } 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';
import ToolExamples, {
CardExampleType
} from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
import { FormikProps } from 'formik';
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'
}
];
const exampleCards: CardExampleType<typeof initialValues>[] = [
{
title: 'Split German Numbers',
description:
'In this example, we break the text into pieces by two characters a comma and space. As a result, we get a column of numbers from 1 to 10 in German.',
sampleText: `1 - eins, 2 - zwei, 3 - drei, 4 - vier, 5 - fünf, 6 - sechs, 7 - sieben, 8 - acht, 9 - neun, 10 - zehn`,
sampleResult: `1 - eins
2 - zwei
3 - drei
4 - vier
5 - fünf
6 - sechs
7 - sieben
8 - acht
9 - neun
10 - zehn`,
sampleOptions: {
...initialValues,
symbolValue: ',',
splitSeparatorType: 'symbol',
outputSeparator: '\n'
}
},
{
title: 'Text Cleanup via a Regular Expression',
description:
'In this example, we use a super smart regular expression trick to clean-up the text. This regexp finds all non-alphabetic characters and splits the text into pieces by these non-alphabetic chars. As a result, we extract only those parts of the text that contain Latin letters and words.',
sampleText: `Finding%№1.65*;?words()is'12#easy_`,
sampleResult: `Finding
words
is
easy`,
sampleOptions: {
...initialValues,
regexValue: '[^a-zA-Z]+',
splitSeparatorType: 'regex',
outputSeparator: '\n'
}
},
{
title: 'Three-dot Output Separator',
description:
'This example splits the text by spaces and then places three dots between the words.',
sampleText: `If you started with $0.01 and doubled your money every day, it would take 27 days to become a millionaire.`,
sampleResult: `If...you...started...with...$0.01...and...doubled...your...money...every...day,...it...would...take...27...days...to...become...a...millionaire.!`,
sampleOptions: {
...initialValues,
symbolValue: '',
splitSeparatorType: 'symbol',
outputSeparator: '...'
}
}
];
export default function SplitText({ title }: ToolComponentProps) {
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
)
);
};
const getGroups: GetGroupsType<typeof initialValues> = ({
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}
/>
))
}
];
return (
<Box>
<ToolInputAndResult
input={<ToolTextInput value={input} onChange={setInput} />}
result={<ToolTextResult title={'Text pieces'} value={result} />}
/>
<ToolOptions
compute={computeExternal}
getGroups={getGroups}
initialValues={initialValues}
input={input}
/>
<ToolExamples
title={title}
exampleCards={exampleCards}
getGroups={getGroups}
formRef={formRef}
setInput={setInput}
/>
</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',
icon: 'material-symbols-light:arrow-split',
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,147 @@
import { Box } from '@mui/material';
import { useState } from 'react';
import ToolTextResult from '@components/result/ToolTextResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import { replaceText } from './service';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import ToolTextInput from '@components/input/ToolTextInput';
import SimpleRadio from '@components/options/SimpleRadio';
import { initialValues, InitialValuesType } from './initialValues';
import ToolContent from '@components/ToolContent';
import { CardExampleType } from '@components/examples/ToolExamples';
import { ToolComponentProps } from '@tools/defineTool';
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Replace specific word in text',
description:
'In this example we will replace the word "hello" with the word "hi". This example doesn\'t use regular expressions.',
sampleText: 'hello, how are you today? hello!',
sampleResult: 'hi, how are you today? hi!',
sampleOptions: {
textToReplace: 'hello, how are you today? hello!',
searchValue: 'hello',
searchRegexp: '',
replaceValue: 'hi',
mode: 'text'
}
},
{
title: 'Replace all numbers in text',
description:
'In this example we will replace all numbers in numbers with * using regexp. In the output we will get text with numbers replaced with *.',
sampleText: 'The price is 100$, and the discount is 20%.',
sampleResult: 'The price is X$, and the discount is X%.',
sampleOptions: {
textToReplace: 'The price is 100$, and the discount is 20%.',
searchValue: '',
searchRegexp: '/\\d+/g',
replaceValue: '*',
mode: 'regexp'
}
},
{
title: 'Replace all dates in text',
description:
'In this example we will replace all dates in the format YYYY-MM-DD with the word DATE using regexp. The output will have all the dates replaced with the word DATE.',
sampleText:
'The event will take place on 2025-03-10, and the deadline is 2025-03-15.',
sampleResult:
'The event will take place on DATE, and the deadline is DATE.',
sampleOptions: {
textToReplace:
'The event will take place on 2025-03-10, and the deadline is 2025-03-15.',
searchValue: '',
searchRegexp: '/\\d{4}-\\d{2}-\\d{2}/g',
replaceValue: 'DATE',
mode: 'regexp'
}
}
];
export default function Replacer({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
function compute(optionsValues: InitialValuesType, input: string) {
setResult(replaceText(optionsValues, input));
}
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Search text',
component: (
<Box>
<SimpleRadio
onClick={() => updateField('mode', 'text')}
checked={values.mode === 'text'}
title={'Find This Pattern in Text'}
/>
<TextFieldWithDesc
description={'Enter the text pattern that you want to replace.'}
value={values.searchValue}
onOwnChange={(val) => updateField('searchValue', val)}
type={'text'}
/>
<SimpleRadio
onClick={() => updateField('mode', 'regexp')}
checked={values.mode === 'regexp'}
title={'Find a Pattern Using a RegExp'}
/>
<TextFieldWithDesc
description={
'Enter the regular expression that you want to replace.'
}
value={values.searchRegexp}
onOwnChange={(val) => updateField('searchRegexp', val)}
type={'text'}
/>
</Box>
)
},
{
title: 'Replace Text',
component: (
<Box>
<TextFieldWithDesc
description={'Enter the pattern to use for replacement.'}
placeholder={'New text'}
value={values.replaceValue}
onOwnChange={(val) => updateField('replaceValue', val)}
type={'text'}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
initialValues={initialValues}
getGroups={getGroups}
compute={compute}
input={input}
setInput={setInput}
inputComponent={
<ToolTextInput
title="Text to replace"
value={input}
onChange={setInput}
/>
}
resultComponent={
<ToolTextResult title={'Text with replacements'} value={result} />
}
toolInfo={{
title: 'Text Replacer',
description:
'Easily replace specific text in your content with this simple, browser-based tool. Just input your text, set the text you want to replace and the replacement value, and instantly get the updated version.'
}}
exampleCards={exampleCards}
/>
);
}

View File

@@ -0,0 +1,15 @@
export type InitialValuesType = {
textToReplace: string;
searchValue: string;
searchRegexp: string;
replaceValue: string;
mode: 'text' | 'regexp';
};
export const initialValues: InitialValuesType = {
textToReplace: '',
searchValue: '',
searchRegexp: '',
replaceValue: '',
mode: 'text'
};

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Text Replacer',
path: 'replacer',
shortDescription: 'Quickly replace text in your content',
icon: 'material-symbols-light:find-replace',
description:
'Easily replace specific text in your content with this simple, browser-based tool. Just input your text, set the text you want to replace and the replacement value, and instantly get the updated version.',
keywords: ['text', 'replace'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,175 @@
import { describe, expect, it } from 'vitest';
import { replaceText } from './service';
import { initialValues } from './initialValues';
describe('replaceText function (text mode)', () => {
const mode = 'text';
it('should replace the word in the text correctly', () => {
const text = 'Lorem ipsum odor amet, consectetuer adipiscing elit.';
const searchValue = 'ipsum';
const replaceValue = 'vitae';
const result = replaceText(
{ ...initialValues, searchValue, replaceValue, mode },
text
);
expect(result).toBe('Lorem vitae odor amet, consectetuer adipiscing elit.');
});
it('should replace letters in the text correctly', () => {
const text =
'Luctus penatibus montes elementum lacus mus vivamus lacus laoreet.';
const searchValue = 'e';
const replaceValue = 'u';
const result = replaceText(
{ ...initialValues, searchValue, replaceValue, mode },
text
);
expect(result).toBe(
'Luctus punatibus montus ulumuntum lacus mus vivamus lacus laoruut.'
);
});
it('should return the original text if one of the required arguments is an empty string', () => {
const text =
'Nostra netus quisque ornare neque dolor sem nostra venenatis.';
expect(
replaceText(
{ ...initialValues, searchValue: '', replaceValue: 'test', mode },
text
)
).toBe('Nostra netus quisque ornare neque dolor sem nostra venenatis.');
expect(
replaceText(
{ ...initialValues, searchValue: 'ornare', replaceValue: 'test', mode },
''
)
).toBe('');
});
it('should replace multiple occurrences of the word correctly', () => {
const text = 'apple orange apple banana apple';
const searchValue = 'apple';
const replaceValue = 'grape';
const result = replaceText(
{ ...initialValues, searchValue, replaceValue, mode },
text
);
expect(result).toBe('grape orange grape banana grape');
});
it('should return the original text if the replace value is an empty string', () => {
const text = 'apple orange apple banana apple';
const searchValue = 'apple';
const replaceValue = '';
const result = replaceText(
{ ...initialValues, searchValue, replaceValue, mode },
text
);
expect(result).toBe(' orange banana ');
});
it('should return the original text if the search value is not found', () => {
const text = 'apple orange banana';
const searchValue = 'grape';
const replaceValue = 'melon';
const result = replaceText(
{ ...initialValues, searchValue, replaceValue, mode },
text
);
expect(result).toBe('apple orange banana');
});
});
describe('replaceText function (regexp mode)', () => {
const mode = 'regexp';
it('should replace a word in text using regexp correctly', () => {
const text = 'Egestas lobortis facilisi convallis rhoncus nunc.';
const searchRegexp = '/nunc/';
const replaceValue = 'hello';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe('Egestas lobortis facilisi convallis rhoncus hello.');
});
it('should replace all words in the text with regexp correctly', () => {
const text =
'Parturient porta ultricies tellus ultricies suscipit quisque torquent.';
const searchRegexp = '/ultricies/g';
const replaceValue = 'hello';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe(
'Parturient porta hello tellus hello suscipit quisque torquent.'
);
});
it('should replace words in text with regexp using alternation operator correctly', () => {
const text =
'Commodo maximus nullam dis placerat fermentum curabitur semper.';
const searchRegexp = '/nullam|fermentum/g';
const replaceValue = 'test';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe(
'Commodo maximus test dis placerat test curabitur semper.'
);
});
it('should return the original text when passed an invalid regexp', () => {
const text =
'Commodo maximus nullam dis placerat fermentum curabitur semper.';
const searchRegexp = '/(/';
const replaceValue = 'test';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe(
'Commodo maximus nullam dis placerat fermentum curabitur semper.'
);
});
it('should remove brackets from text correctly using regexp', () => {
const text =
'Porta nulla (magna) lectus, [taciti] habitant nunc urna maximus metus.';
const searchRegexp = '/[()\\[\\]]/g';
const replaceValue = '';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe(
'Porta nulla magna lectus, taciti habitant nunc urna maximus metus.'
);
});
it('should replace case-insensitive words correctly', () => {
const text = 'Porta cras ad laoreet porttitor feRmeNtum consectetur?';
const searchRegexp = '/porta|fermentum/gi';
const replaceValue = 'test';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe('test cras ad laoreet porttitor test consectetur?');
});
it('should replace words with digits and symbols correctly', () => {
const text = 'The price is 100$, and the discount is 20%.';
const searchRegexp = '/\\d+/g';
const replaceValue = 'X';
const result = replaceText(
{ ...initialValues, searchRegexp, replaceValue, mode },
text
);
expect(result).toBe('The price is X$, and the discount is X%.');
});
});

View File

@@ -0,0 +1,40 @@
import { InitialValuesType } from './initialValues';
function isFieldsEmpty(textField: string, searchField: string) {
return !textField.trim() || !searchField.trim();
}
export function replaceText(options: InitialValuesType, text: string) {
const { searchValue, searchRegexp, replaceValue, mode } = options;
switch (mode) {
case 'text':
if (isFieldsEmpty(text, searchValue)) return text;
return text.replaceAll(searchValue, replaceValue);
case 'regexp':
if (isFieldsEmpty(text, searchRegexp)) return text;
return replaceTextWithRegexp(text, searchRegexp, replaceValue);
}
}
function replaceTextWithRegexp(
text: string,
searchRegexp: string,
replaceValue: string
) {
try {
const match = searchRegexp.match(/^\/(.*)\/([a-z]*)$/i);
if (match) {
// Input is in /pattern/flags format
const [, pattern, flags] = match;
return text.replace(new RegExp(pattern, flags), replaceValue);
} else {
// Input is a raw pattern - don't escape it
return text.replace(new RegExp(searchRegexp, 'g'), replaceValue);
}
} catch (err) {
console.error('Invalid regular expression:', err);
return text;
}
}

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',
icon: 'arcticons:morse',
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'))
});

View 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);
};

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } 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);
});
});

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 Uppercase() {
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: 'Uppercase',
path: 'uppercase',
icon: '',
description: '',
shortDescription: '',
keywords: ['uppercase'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,3 @@
export function UppercaseInput(input: string): string {
return input.toUpperCase();
}

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { UppercaseInput } from './service';
describe('UppercaseInput', () => {
it('should convert a lowercase string to uppercase', () => {
const input = 'hello';
const result = UppercaseInput(input);
expect(result).toBe('HELLO');
});
it('should convert a mixed case string to uppercase', () => {
const input = 'HeLLo WoRLd';
const result = UppercaseInput(input);
expect(result).toBe('HELLO WORLD');
});
it('should convert an already uppercase string to uppercase', () => {
const input = 'HELLO';
const result = UppercaseInput(input);
expect(result).toBe('HELLO');
});
it('should handle an empty string', () => {
const input = '';
const result = UppercaseInput(input);
expect(result).toBe('');
});
it('should handle a string with numbers and symbols', () => {
const input = '123 hello! @world';
const result = UppercaseInput(input);
expect(result).toBe('123 HELLO! @WORLD');
});
});