mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-22 07:29:39 +02:00
Merge remote-tracking branch 'origin/main' into truncate
# Conflicts: # src/pages/tools/string/index.ts
This commit is contained in:
@@ -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('');
|
||||
});
|
||||
});
|
11
src/pages/tools/string/create-palindrome/index.tsx
Normal file
11
src/pages/tools/string/create-palindrome/index.tsx
Normal 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>;
|
||||
}
|
13
src/pages/tools/string/create-palindrome/meta.ts
Normal file
13
src/pages/tools/string/create-palindrome/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: 'Create palindrome',
|
||||
path: 'create-palindrome',
|
||||
icon: '',
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['create', 'palindrome'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
36
src/pages/tools/string/create-palindrome/service.ts
Normal file
36
src/pages/tools/string/create-palindrome/service.ts
Normal 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');
|
||||
}
|
@@ -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');
|
||||
});
|
||||
});
|
11
src/pages/tools/string/extract-substring/index.tsx
Normal file
11
src/pages/tools/string/extract-substring/index.tsx
Normal 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>;
|
||||
}
|
13
src/pages/tools/string/extract-substring/meta.ts
Normal file
13
src/pages/tools/string/extract-substring/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: 'Extract substring',
|
||||
path: 'extract-substring',
|
||||
icon: '',
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['extract', 'substring'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
36
src/pages/tools/string/extract-substring/service.ts
Normal file
36
src/pages/tools/string/extract-substring/service.ts
Normal 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');
|
||||
}
|
30
src/pages/tools/string/index.ts
Normal file
30
src/pages/tools/string/index.ts
Normal 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
|
||||
];
|
186
src/pages/tools/string/join/index.tsx
Normal file
186
src/pages/tools/string/join/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
13
src/pages/tools/string/join/meta.ts
Normal file
13
src/pages/tools/string/join/meta.ts
Normal 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'))
|
||||
});
|
17
src/pages/tools/string/join/service.ts
Normal file
17
src/pages/tools/string/join/service.ts
Normal 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);
|
||||
}
|
18
src/pages/tools/string/join/string-join.e2e.spec.ts
Normal file
18
src/pages/tools/string/join/string-join.e2e.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
64
src/pages/tools/string/join/string-join.service.test.ts
Normal file
64
src/pages/tools/string/join/string-join.service.test.ts
Normal 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);
|
||||
});
|
||||
});
|
11
src/pages/tools/string/palindrome/index.tsx
Normal file
11
src/pages/tools/string/palindrome/index.tsx
Normal 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>;
|
||||
}
|
13
src/pages/tools/string/palindrome/meta.ts
Normal file
13
src/pages/tools/string/palindrome/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: 'Palindrome',
|
||||
path: 'palindrome',
|
||||
icon: '',
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['palindrome'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
60
src/pages/tools/string/palindrome/palindrome.service.test.ts
Normal file
60
src/pages/tools/string/palindrome/palindrome.service.test.ts
Normal 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');
|
||||
});
|
||||
});
|
41
src/pages/tools/string/palindrome/service.ts
Normal file
41
src/pages/tools/string/palindrome/service.ts
Normal 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);
|
||||
}
|
11
src/pages/tools/string/randomize-case/index.tsx
Normal file
11
src/pages/tools/string/randomize-case/index.tsx
Normal 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>;
|
||||
}
|
13
src/pages/tools/string/randomize-case/meta.ts
Normal file
13
src/pages/tools/string/randomize-case/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: 'Randomize case',
|
||||
path: 'randomize-case',
|
||||
icon: '',
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['randomize', 'case'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
8
src/pages/tools/string/randomize-case/service.ts
Normal file
8
src/pages/tools/string/randomize-case/service.ts
Normal 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('');
|
||||
}
|
260
src/pages/tools/string/remove-duplicate-lines/index.tsx
Normal file
260
src/pages/tools/string/remove-duplicate-lines/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
13
src/pages/tools/string/remove-duplicate-lines/meta.ts
Normal file
13
src/pages/tools/string/remove-duplicate-lines/meta.ts
Normal 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'))
|
||||
});
|
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
88
src/pages/tools/string/remove-duplicate-lines/service.ts
Normal file
88
src/pages/tools/string/remove-duplicate-lines/service.ts
Normal 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
|
||||
// });
|
114
src/pages/tools/string/repeat/index.tsx
Normal file
114
src/pages/tools/string/repeat/index.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
11
src/pages/tools/string/repeat/initialValues.ts
Normal file
11
src/pages/tools/string/repeat/initialValues.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type InitialValuesType = {
|
||||
textToRepeat: string;
|
||||
repeatAmount: string;
|
||||
delimiter: string;
|
||||
};
|
||||
|
||||
export const initialValues: InitialValuesType = {
|
||||
textToRepeat: '',
|
||||
repeatAmount: '5',
|
||||
delimiter: ''
|
||||
};
|
13
src/pages/tools/string/repeat/meta.ts
Normal file
13
src/pages/tools/string/repeat/meta.ts
Normal 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'))
|
||||
});
|
55
src/pages/tools/string/repeat/repeatText.service.test.ts
Normal file
55
src/pages/tools/string/repeat/repeatText.service.test.ts
Normal 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('');
|
||||
});
|
||||
});
|
9
src/pages/tools/string/repeat/service.ts
Normal file
9
src/pages/tools/string/repeat/service.ts
Normal 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);
|
||||
}
|
11
src/pages/tools/string/reverse/index.tsx
Normal file
11
src/pages/tools/string/reverse/index.tsx
Normal 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>;
|
||||
}
|
13
src/pages/tools/string/reverse/meta.ts
Normal file
13
src/pages/tools/string/reverse/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: 'Reverse',
|
||||
path: 'reverse',
|
||||
icon: '',
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['reverse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
52
src/pages/tools/string/reverse/reverse.service.test.ts
Normal file
52
src/pages/tools/string/reverse/reverse.service.test.ts
Normal 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');
|
||||
});
|
||||
});
|
30
src/pages/tools/string/reverse/service.ts
Normal file
30
src/pages/tools/string/reverse/service.ts
Normal 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');
|
||||
}
|
218
src/pages/tools/string/split/index.tsx
Normal file
218
src/pages/tools/string/split/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
14
src/pages/tools/string/split/meta.ts
Normal file
14
src/pages/tools/string/split/meta.ts
Normal 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'))
|
||||
});
|
65
src/pages/tools/string/split/service.ts
Normal file
65
src/pages/tools/string/split/service.ts
Normal 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);
|
||||
}
|
72
src/pages/tools/string/split/string-split.service.test.ts
Normal file
72
src/pages/tools/string/split/string-split.service.test.ts
Normal 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');
|
||||
});
|
||||
});
|
147
src/pages/tools/string/text-replacer/index.tsx
Normal file
147
src/pages/tools/string/text-replacer/index.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
15
src/pages/tools/string/text-replacer/initialValues.ts
Normal file
15
src/pages/tools/string/text-replacer/initialValues.ts
Normal 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'
|
||||
};
|
13
src/pages/tools/string/text-replacer/meta.ts
Normal file
13
src/pages/tools/string/text-replacer/meta.ts
Normal 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'))
|
||||
});
|
175
src/pages/tools/string/text-replacer/replaceText.service.test.ts
Normal file
175
src/pages/tools/string/text-replacer/replaceText.service.test.ts
Normal 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%.');
|
||||
});
|
||||
});
|
40
src/pages/tools/string/text-replacer/service.ts
Normal file
40
src/pages/tools/string/text-replacer/service.ts
Normal 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;
|
||||
}
|
||||
}
|
63
src/pages/tools/string/to-morse/index.tsx
Normal file
63
src/pages/tools/string/to-morse/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
14
src/pages/tools/string/to-morse/meta.ts
Normal file
14
src/pages/tools/string/to-morse/meta.ts
Normal 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'))
|
||||
});
|
9
src/pages/tools/string/to-morse/service.ts
Normal file
9
src/pages/tools/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/tools/string/to-morse/to-morse.service.test.ts
Normal file
50
src/pages/tools/string/to-morse/to-morse.service.test.ts
Normal 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);
|
||||
});
|
||||
});
|
11
src/pages/tools/string/uppercase/index.tsx
Normal file
11
src/pages/tools/string/uppercase/index.tsx
Normal 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>;
|
||||
}
|
13
src/pages/tools/string/uppercase/meta.ts
Normal file
13
src/pages/tools/string/uppercase/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: 'Uppercase',
|
||||
path: 'uppercase',
|
||||
icon: '',
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['uppercase'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
3
src/pages/tools/string/uppercase/service.ts
Normal file
3
src/pages/tools/string/uppercase/service.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function UppercaseInput(input: string): string {
|
||||
return input.toUpperCase();
|
||||
}
|
34
src/pages/tools/string/uppercase/uppercase.service.test.ts
Normal file
34
src/pages/tools/string/uppercase/uppercase.service.test.ts
Normal 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');
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user