Merge pull request #322 from bhavesh158/slug-generator

feat: add slug generator tool
This commit is contained in:
Chesterkxng
2026-04-07 14:37:07 +02:00
committed by GitHub
7 changed files with 224 additions and 1 deletions
+13
View File
@@ -323,5 +323,18 @@
"title": "Unicode Encoder / Decoder",
"description": "This tool lets you convert plain text into Unicode escape sequences (e.g., \\uXXXX) and decode Unicode escape sequences back into standard text. You can also choose whether the hexadecimal output is formatted in uppercase or lowercase when encoding."
}
},
"slugGenerator": {
"title": "Slug Generator",
"description": "Convert any text into a URL-friendly slug.",
"shortDescription": "Generate URL-friendly slugs from text.",
"longDescription": "Convert any text into clean, SEO-friendly slugs for use in URLs. The tool handles spaces, special characters, accented letters, and multiple lines at once — making it easy to batch-process blog post titles, product names, or page headings into consistent, readable URL segments.",
"inputTitle": "Input Text",
"resultTitle": "URL Slugs",
"options": {
"title": "Slug Options",
"caseLabel": "Case Sensitive",
"caseDescription": "Preserve the original casing of the input instead of converting to lowercase"
}
}
}
+3 -1
View File
@@ -1,3 +1,4 @@
import { tool as stringSlugGenerator } from './slug-generator/meta';
import { tool as stringHiddenCharacterDetector } from './hidden-character-detector/meta';
import { tool as stringRemoveDuplicateLines } from './remove-duplicate-lines/meta';
import { tool as stringRotate } from './rotate/meta';
@@ -47,5 +48,6 @@ export const stringTools = [
stringEncodeUrl,
StringDecodeUrl,
stringUnicode,
stringHiddenCharacterDetector
stringHiddenCharacterDetector,
stringSlugGenerator
];
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import { GetGroupsType } from '@components/options/ToolOptions';
import { CardExampleType } from '@components/examples/ToolExamples';
import { slugGenerator } from './service';
import { InitialValuesType } from './types';
import { useTranslation } from 'react-i18next';
const initialValues: InitialValuesType = {
caseSensitive: false
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Slug (lowercase)',
description: 'Convert a title into a URL-friendly slug',
sampleText: 'Hello World! This is a test.',
sampleResult: 'hello-world-this-is-a-test',
sampleOptions: {
caseSensitive: false
}
},
{
title: 'Slug (case preserved)',
description:
'Convert a title into a URL-friendly slug without any case change',
sampleText: 'Hello World! This is a test.',
sampleResult: 'Hello-World-This-is-a-test',
sampleOptions: {
caseSensitive: true
}
},
{
title: 'Slugs (multi lines processing)',
description: 'Convert titles on each lines into a URL-friendly slugs',
sampleText: 'Hello World! This is a test.\nMultiples lines are processed',
sampleResult: 'Hello-World-This-is-a-test\nMultiples-lines-are-processed',
sampleOptions: {
caseSensitive: true
}
}
];
export default function SlugGenerator({
title,
longDescription
}: ToolComponentProps) {
const { t } = useTranslation('string');
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
const compute = (values: InitialValuesType, input: string) => {
setResult(slugGenerator(input, values));
};
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: t('slugGenerator.options.title'),
component: [
<CheckboxWithDesc
key="case"
checked={values.caseSensitive}
title={t('slugGenerator.options.caseLabel')}
description={t('slugGenerator.options.caseDescription')}
onChange={(val) => updateField('caseSensitive', val)}
/>
]
}
];
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolTextInput
value={input}
onChange={setInput}
title={t('slugGenerator.inputTitle')}
/>
}
resultComponent={
<ToolTextResult value={result} title={t('slugGenerator.resultTitle')} />
}
initialValues={initialValues}
exampleCards={exampleCards}
getGroups={getGroups}
setInput={setInput}
compute={compute}
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
/>
);
}
@@ -0,0 +1,16 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
i18n: {
name: 'string:slugGenerator.title',
description: 'string:slugGenerator.description',
shortDescription: 'string:slugGenerator.shortDescription',
longDescription: 'string:slugGenerator.longDescription',
userTypes: ['developers', 'generalUsers']
},
path: 'slug-generator',
icon: 'solar:link-bold-duotone',
keywords: ['slug', 'generator', 'url', 'seo'],
component: lazy(() => import('./index'))
});
@@ -0,0 +1,21 @@
import { InitialValuesType } from './types';
function compute(input: string, options: InitialValuesType): string {
return (!options.caseSensitive ? input.toLowerCase() : input)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function slugGenerator(
input: string,
options: InitialValuesType
): string {
return input
.split('\n')
.map((line) => (line.trim() === '' ? '' : compute(line, options)))
.join('\n');
}
@@ -0,0 +1,70 @@
import { expect, describe, it } from 'vitest';
import { slugGenerator } from './service';
describe('slug-generator', () => {
const defaultOptions = { caseSensitive: false };
const caseSensitiveOptions = { caseSensitive: true };
// Basic conversion
it('should convert to lowercase', () => {
expect(slugGenerator('Hello World', defaultOptions)).toBe('hello-world');
});
it('should preserve casing when caseSensitive is true', () => {
expect(slugGenerator('Hello World', caseSensitiveOptions)).toBe(
'Hello-World'
);
});
// Spaces and dashes
it('should replace spaces with dashes', () => {
expect(slugGenerator('hello world', defaultOptions)).toBe('hello-world');
});
it('should handle multiple spaces and dashes', () => {
expect(slugGenerator('hello world--test', defaultOptions)).toBe(
'hello-world-test'
);
});
it('should trim dashes from ends', () => {
expect(slugGenerator(' hello world ', defaultOptions)).toBe(
'hello-world'
);
});
// Special characters
it('should remove special characters', () => {
expect(slugGenerator('hello world!', defaultOptions)).toBe('hello-world');
});
it('should handle non-alphanumeric characters', () => {
expect(slugGenerator('Hello World @2023!', defaultOptions)).toBe(
'hello-world-2023'
);
});
// Accents
it('should normalize accented characters', () => {
expect(slugGenerator('Héllo Wörld', defaultOptions)).toBe('hello-world');
});
it('should normalize accents while preserving case when caseSensitive is true', () => {
expect(slugGenerator('Héllo Wörld', caseSensitiveOptions)).toBe(
'Hello-World'
);
});
// Multi-line
it('should handle multiple lines', () => {
expect(slugGenerator('Hello World\nFoo Bar', defaultOptions)).toBe(
'hello-world\nfoo-bar'
);
});
it('should preserve empty lines', () => {
expect(slugGenerator('Hello World\n\nFoo Bar', defaultOptions)).toBe(
'hello-world\n\nfoo-bar'
);
});
});
@@ -0,0 +1,3 @@
export type InitialValuesType = {
caseSensitive: boolean;
};