Merge pull request #48 from EugSh1/truncate-text

feat: add Text Truncate tool
This commit is contained in:
Ibrahima G. Coulibaly
2025-03-11 18:33:58 +00:00
committed by GitHub
6 changed files with 419 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ 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';
import { tool as stringTruncate } from './truncate/meta';
export const stringTools = [
stringSplit,
@@ -21,6 +22,7 @@ export const stringTools = [
stringToMorse,
stringReplace,
stringRepeat,
stringTruncate,
stringReverse,
stringRandomizeCase,
stringUppercase,

View File

@@ -0,0 +1,166 @@
import { Box } from '@mui/material';
import { useState } from 'react';
import ToolTextResult from '@components/result/ToolTextResult';
import { GetGroupsType } from '@components/options/ToolOptions';
import { truncateText } 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';
import SimpleRadio from '@components/options/SimpleRadio';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Basic Truncation on the Right',
description: 'Truncate text from the right side based on max length.',
sampleText: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
sampleResult: 'Lorem ipsum dolor...',
sampleOptions: {
...initialValues,
textToTruncate:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
maxLength: '20',
truncationSide: 'right',
addIndicator: true,
indicator: '...'
}
},
{
title: 'Truncation on the Left with Indicator',
description: 'Truncate text from the left side and add an indicator.',
sampleText: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
sampleResult: '...is dolor sit amet, consectetur adipiscing elit.',
sampleOptions: {
...initialValues,
textToTruncate:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
maxLength: '40',
truncationSide: 'left',
addIndicator: true,
indicator: '...'
}
},
{
title: 'Multi-line Truncation with Indicator',
description:
'Truncate text line by line, adding an indicator to each line.',
sampleText: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.`,
sampleResult: `Lorem ipsum dolor sit amet, consectetur...
Ut enim ad minim veniam, quis nostrud...
Duis aute irure dolor in reprehenderit...`,
sampleOptions: {
...initialValues,
textToTruncate: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.`,
maxLength: '50',
lineByLine: true,
addIndicator: true,
indicator: '...'
}
}
];
export default function Truncate({ title }: ToolComponentProps) {
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
function compute(optionsValues: InitialValuesType, input: string) {
setResult(truncateText(optionsValues, input));
}
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Truncation Side',
component: (
<Box>
<SimpleRadio
onClick={() => updateField('truncationSide', 'right')}
checked={values.truncationSide === 'right'}
title={'Right-side Truncation'}
description={'Remove characters from the end of the text.'}
/>
<SimpleRadio
onClick={() => updateField('truncationSide', 'left')}
checked={values.truncationSide === 'left'}
title={'Left-side Truncation'}
description={'Remove characters from the start of the text.'}
/>
</Box>
)
},
{
title: 'Length and Lines',
component: (
<Box>
<TextFieldWithDesc
description={'Number of characters to leave in the text.'}
placeholder="Number"
value={values.maxLength}
onOwnChange={(val) => updateField('maxLength', val)}
type={'number'}
/>
<CheckboxWithDesc
onChange={(val) => updateField('lineByLine', val)}
checked={values.lineByLine}
title={'Line-by-line Truncating'}
description={'Truncate each line separately.'}
/>
</Box>
)
},
{
title: 'Suffix and Affix',
component: (
<Box>
<CheckboxWithDesc
onChange={(val) => updateField('addIndicator', val)}
checked={values.addIndicator}
title={'Add Truncation Indicator'}
description={''}
/>
<TextFieldWithDesc
description={
'Characters to add at the end (or start) of the text. Note: They count towards the length.'
}
placeholder="Characters"
value={values.indicator}
onOwnChange={(val) => updateField('indicator', 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={'Truncated text'} value={result} />
}
toolInfo={{
title: 'Truncate text',
description:
'Load your text in the input form on the left and you will automatically get truncated text on the right.'
}}
exampleCards={exampleCards}
/>
);
}

View File

@@ -0,0 +1,19 @@
export type truncationSideType = 'right' | 'left';
export type InitialValuesType = {
textToTruncate: string;
truncationSide: truncationSideType;
maxLength: string;
lineByLine: boolean;
addIndicator: boolean;
indicator: string;
};
export const initialValues: InitialValuesType = {
textToTruncate: '',
truncationSide: 'right',
maxLength: '15',
lineByLine: false,
addIndicator: false,
indicator: ''
};

View File

@@ -0,0 +1,13 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
name: 'Truncate text',
path: 'truncate',
shortDescription: 'Truncate your text easily',
icon: 'material-symbols-light:short-text',
description:
'Load your text in the input form on the left and you will automatically get truncated text on the right.',
keywords: ['text', 'truncate'],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,84 @@
import { InitialValuesType, truncationSideType } from './initialValues';
export function truncateText(options: InitialValuesType, text: string) {
const { truncationSide, maxLength, lineByLine, addIndicator, indicator } =
options;
const parsedMaxLength = parseInt(maxLength) || 0;
if (parsedMaxLength < 0) {
throw new Error('Length value cannot be negative');
}
const truncate =
truncationSide === 'right' ? truncateFromRight : truncateFromLeft;
return lineByLine
? text
.split('\n')
.map((line) =>
truncate(
line,
parsedMaxLength,
addIndicator,
indicator,
truncationSide
)
)
.join('\n')
: truncate(text, parsedMaxLength, addIndicator, indicator, truncationSide);
}
function truncateFromRight(
text: string,
maxLength: number,
addIndicator: boolean,
indicator: string,
truncationSide: truncationSideType
) {
const result = text.slice(0, maxLength);
return addIndicator
? addIndicatorToText(result, indicator, truncationSide)
: result;
}
function truncateFromLeft(
text: string,
maxLength: number,
addIndicator: boolean,
indicator: string,
truncationSide: truncationSideType
) {
const result = text.slice(-maxLength);
return addIndicator
? addIndicatorToText(result, indicator, truncationSide)
: result;
}
function addIndicatorToText(
text: string,
indicator: string,
truncationSide: truncationSideType
) {
if (indicator.length > text.length && text.length) {
throw new Error('Indicator length is greater than truncation length');
}
if (!text.length) {
return '';
}
switch (truncationSide) {
case 'right': {
const result = text.slice(0, text.length - indicator.length);
return `${result}${indicator}`;
}
case 'left': {
const result = text.slice(-text.length + indicator.length);
return `${indicator}${result}`;
}
}
}

View File

@@ -0,0 +1,135 @@
import { describe, expect, it } from 'vitest';
import { truncateText } from './service';
import { initialValues } from './initialValues';
describe('repeatText function (normal mode)', () => {
const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
it('should truncate the text correctly on the right side', () => {
const maxLength = '30';
const result = truncateText({ ...initialValues, maxLength }, text);
expect(result).toBe('Lorem ipsum dolor sit amet, co');
});
it('should truncate the text correctly on the left side', () => {
const maxLength = '30';
const truncationSide = 'left';
const result = truncateText(
{ ...initialValues, maxLength, truncationSide },
text
);
expect(result).toBe('labore et dolore magna aliqua.');
});
it('should truncate the text and add the indicator correctly on the right side', () => {
const maxLength = '24';
const addIndicator = true;
const indicator = '...';
const result = truncateText(
{ ...initialValues, maxLength, addIndicator, indicator },
text
);
expect(result).toBe('Lorem ipsum dolor sit...');
});
it('should truncate the text and add the indicator correctly on the left side', () => {
const maxLength = '23';
const truncationSide = 'left';
const addIndicator = true;
const indicator = '...';
const result = truncateText(
{ ...initialValues, maxLength, truncationSide, addIndicator, indicator },
text
);
expect(result).toBe('...dolore magna aliqua.');
});
it('should throw an error if maxLength is less than zero', () => {
const maxLength = '-1';
expect(() =>
truncateText({ ...initialValues, maxLength }, text)
).toThrowError('Length value cannot be negative');
});
it('should throw an error if the indicator length is greater than the truncate length', () => {
const maxLength = '10';
const addIndicator = true;
const indicator = '.'.repeat(11);
expect(() =>
truncateText(
{ ...initialValues, maxLength, addIndicator, indicator },
text
)
).toThrowError('Indicator length is greater than truncation length');
});
});
describe('repeatText function (line by line mode)', () => {
const text = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.`;
const maxLength = '45';
it('should truncate the multi-line text correctly on the right side', () => {
const lineByLine = true;
const result = truncateText(
{ ...initialValues, maxLength, lineByLine },
text
);
expect(result).toBe(`
Lorem ipsum dolor sit amet, consectetur adipi
Ut enim ad minim veniam, quis nostrud exercit
Duis aute irure dolor in reprehenderit in vol`);
});
it('should truncate the multi-line text correctly on the left side', () => {
const truncationSide = 'left';
const lineByLine = true;
const result = truncateText(
{ ...initialValues, maxLength, truncationSide, lineByLine },
text
);
expect(result).toBe(`
incididunt ut labore et dolore magna aliqua.
oris nisi ut aliquip ex ea commodo consequat.
esse cillum dolore eu fugiat nulla pariatur.`);
});
it('should truncate the multi-line and add the indicator text correctly on the right side', () => {
const lineByLine = true;
const addIndicator = true;
const indicator = '...';
const result = truncateText(
{ ...initialValues, maxLength, lineByLine, addIndicator, indicator },
text
);
expect(result).toBe(`
Lorem ipsum dolor sit amet, consectetur ad...
Ut enim ad minim veniam, quis nostrud exer...
Duis aute irure dolor in reprehenderit in ...`);
});
it('should truncate the multi-line and add the indicator text correctly on the left side', () => {
const lineByLine = true;
const truncationSide = 'left';
const addIndicator = true;
const indicator = '...';
const result = truncateText(
{
...initialValues,
maxLength,
truncationSide,
lineByLine,
addIndicator,
indicator
},
text
);
expect(result).toBe(`
...cididunt ut labore et dolore magna aliqua.
...s nisi ut aliquip ex ea commodo consequat.
...se cillum dolore eu fugiat nulla pariatur.`);
});
});