Merge pull request #322 from bhavesh158/slug-generator
feat: add slug generator tool
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user