Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/project/src/util/slugify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
/**
* Convert a human-readable name into a slug used as an identifier.
*
* Any run of characters which is neither a letter, a digit nor an underscore
* becomes a separator. Letters and digits are matched with the Unicode
* properties `\p{L}` and `\p{N}` rather than `\w`, which is ASCII-only and
* would drop every non-Latin character: `café` slugified to `caf`, and a name
* written entirely in a non-Latin script collapsed to an empty string.
*
* For ASCII-only input this produces exactly the same slug as before, so
* identifiers in existing projects are unaffected.
*/
export default function slugify(text: string) {
return (
text?.replace(/\W/g, ' ').trim().replace(/\s+/g, '-').toLowerCase() ?? ''
text
?.replace(/[^\p{L}\p{N}_]+/gu, ' ')
.trim()
.replace(/\s+/g, '-')
.toLowerCase() ?? ''
);
}
42 changes: 42 additions & 0 deletions packages/project/test/util/slugify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import test from 'ava';
import slugify from '../../src/util/slugify';

test('lowercases and hyphenates a plain name', (t) => {
t.is(slugify('My Workflow'), 'my-workflow');
});

test('collapses runs of separators', (t) => {
t.is(slugify('My Workflow -- Two'), 'my-workflow-two');
});

test('keeps underscores and digits', (t) => {
t.is(slugify('step_1 of 2'), 'step_1-of-2');
});

test('trims leading and trailing separators', (t) => {
t.is(slugify(' ...My Workflow! '), 'my-workflow');
});

test('keeps accented latin characters', (t) => {
t.is(slugify('café'), 'café');
t.is(slugify('résumé'), 'résumé');
});

test('keeps a whole accented phrase readable', (t) => {
t.is(slugify("Vérifier l'état du patient"), 'vérifier-l-état-du-patient');
});

test('keeps non-latin scripts instead of collapsing to an empty string', (t) => {
t.is(slugify('患者確認'), '患者確認');
t.is(slugify('проверка пациента'), 'проверка-пациента');
t.is(slugify('التحقق من المريض'), 'التحقق-من-المريض');
});

test('still drops characters which are neither letters nor digits', (t) => {
t.is(slugify('deploy 🚀 now'), 'deploy-now');
});

test('handles empty and nullish input', (t) => {
t.is(slugify(''), '');
t.is(slugify(undefined as unknown as string), '');
});
3 changes: 3 additions & 0 deletions packages/project/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"include": ["src/**/*.ts", "test/**/*test.ts"],
"compilerOptions": {
"module": "ESNext",
// Unicode property escapes in slugify need ES2018 or later. Build output is
// controlled by tsup; this only affects type validation.
"target": "ES2020",
"sourceMap": true
}
}