39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
export function generateSlug(text: string): string {
|
|
return text
|
|
.toString()
|
|
.toLowerCase()
|
|
.trim()
|
|
.normalize('NFD') // remove accents
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/\s+/g, '-') // replace spaces with -
|
|
.replace(/[^\w-]+/g, '') // remove all non-word chars
|
|
.replace(/--+/g, '-') // replace multiple - with single -
|
|
.replace(/^-+/, '') // trim - from start of text
|
|
.replace(/-+$/, ''); // trim - from end of text
|
|
}
|
|
|
|
/**
|
|
* Validates external or relative URLs to prevent javascript:/data:/vbscript: XSS in href attributes.
|
|
*/
|
|
export function safeUrl(url?: string | null): string {
|
|
if (!url || typeof url !== 'string') return '#';
|
|
const trimmed = url.trim();
|
|
if (!trimmed) return '#';
|
|
|
|
// Allow safe relative paths (starting with single `/` and not `//` or `/\`)
|
|
if (trimmed.startsWith('/') && !trimmed.startsWith('//') && !trimmed.startsWith('/\\')) {
|
|
return trimmed;
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(trimmed);
|
|
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
return parsed.toString();
|
|
}
|
|
} catch {
|
|
// Invalid URL
|
|
}
|
|
|
|
return '#';
|
|
}
|