diff --git a/src/pages/tools/string/index.ts b/src/pages/tools/string/index.ts index f9ba4ec..b2aa502 100644 --- a/src/pages/tools/string/index.ts +++ b/src/pages/tools/string/index.ts @@ -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, diff --git a/src/pages/tools/string/truncate/index.tsx b/src/pages/tools/string/truncate/index.tsx new file mode 100644 index 0000000..8f60b6a --- /dev/null +++ b/src/pages/tools/string/truncate/index.tsx @@ -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[] = [ + { + 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(''); + const [result, setResult] = useState(''); + + function compute(optionsValues: InitialValuesType, input: string) { + setResult(truncateText(optionsValues, input)); + } + + const getGroups: GetGroupsType = ({ + values, + updateField + }) => [ + { + title: 'Truncation Side', + component: ( + + updateField('truncationSide', 'right')} + checked={values.truncationSide === 'right'} + title={'Right-side Truncation'} + description={'Remove characters from the end of the text.'} + /> + updateField('truncationSide', 'left')} + checked={values.truncationSide === 'left'} + title={'Left-side Truncation'} + description={'Remove characters from the start of the text.'} + /> + + ) + }, + { + title: 'Length and Lines', + component: ( + + updateField('maxLength', val)} + type={'number'} + /> + updateField('lineByLine', val)} + checked={values.lineByLine} + title={'Line-by-line Truncating'} + description={'Truncate each line separately.'} + /> + + ) + }, + { + title: 'Suffix and Affix', + component: ( + + updateField('addIndicator', val)} + checked={values.addIndicator} + title={'Add Truncation Indicator'} + description={''} + /> + updateField('indicator', val)} + type={'text'} + /> + + ) + } + ]; + + return ( + + } + resultComponent={ + + } + 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} + /> + ); +} diff --git a/src/pages/tools/string/truncate/initialValues.ts b/src/pages/tools/string/truncate/initialValues.ts new file mode 100644 index 0000000..2484f3b --- /dev/null +++ b/src/pages/tools/string/truncate/initialValues.ts @@ -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: '' +}; diff --git a/src/pages/tools/string/truncate/meta.ts b/src/pages/tools/string/truncate/meta.ts new file mode 100644 index 0000000..1cbb045 --- /dev/null +++ b/src/pages/tools/string/truncate/meta.ts @@ -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')) +}); diff --git a/src/pages/tools/string/truncate/service.ts b/src/pages/tools/string/truncate/service.ts new file mode 100644 index 0000000..637a41a --- /dev/null +++ b/src/pages/tools/string/truncate/service.ts @@ -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}`; + } + } +} diff --git a/src/pages/tools/string/truncate/truncateText.service.test.ts b/src/pages/tools/string/truncate/truncateText.service.test.ts new file mode 100644 index 0000000..e294123 --- /dev/null +++ b/src/pages/tools/string/truncate/truncateText.service.test.ts @@ -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.`); + }); +});