← run

js-01-slugify

1.000
9/9 tests· basic
Challenge · difficulty 1/5
# Slugify a title

Implement an ES module **`solution.js`** exporting a single function:

```js
export function slugify(input) { /* ... */ }
```

Convert a human title string into a URL slug:

- Lowercase the whole string.
- Treat **any character that is not an ASCII letter (`a–z`) or digit (`0–9`) as a separator** —
  this includes spaces, punctuation, underscores, and non-ASCII letters.
- Replace each **run** of one or more separators with a **single** hyphen (`-`).
- Strip any leading or trailing hyphens from the result.

Return the resulting slug (a string). An input made entirely of separators returns `""`.

Examples:
```js
slugify("Hello World")          // => "hello-world"
slugify("  Hello,  World!  ")   // => "hello-world"
slugify("already-clean")        // => "already-clean"
slugify("Top 10 Tips")          // => "top-10-tips"
slugify("a---b__c")             // => "a-b-c"
slugify("@#$%")                 // => ""
```
tests/solution.test.js
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { slugify } from "./solution.js";

test("spaces become single hyphens", () => {
  assert.equal(slugify("Hello World"), "hello-world");
});

test("punctuation collapses and ends are trimmed", () => {
  assert.equal(slugify("  Hello,  World!  "), "hello-world");
});

test("already-clean input is unchanged", () => {
  assert.equal(slugify("already-clean"), "already-clean");
});

test("digits are preserved", () => {
  assert.equal(slugify("Top 10 Tips"), "top-10-tips");
});

test("runs of mixed separators collapse to one hyphen", () => {
  assert.equal(slugify("a---b__c"), "a-b-c");
});

test("leading and trailing junk is trimmed", () => {
  assert.equal(slugify("--Foo Bar--"), "foo-bar");
});

test("all-separator input becomes empty string", () => {
  assert.equal(slugify("@#$%"), "");
});

test("empty string stays empty", () => {
  assert.equal(slugify(""), "");
});

test("non-ASCII letters act as separators", () => {
  assert.equal(slugify("café crème"), "caf-cr-me");
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```

Test output (stdout)

TAP version 13
# Subtest: spaces become single hyphens
ok 1 - spaces become single hyphens
  ---
  duration_ms: 0.416611
  type: 'test'
  ...
# Subtest: punctuation collapses and ends are trimmed
ok 2 - punctuation collapses and ends are trimmed
  ---
  duration_ms: 0.06739
  type: 'test'
  ...
# Subtest: already-clean input is unchanged
ok 3 - already-clean input is unchanged
  ---
  duration_ms: 0.04568
  type: 'test'
  ...
# Subtest: digits are preserved
ok 4 - digits are preserved
  ---
  duration_ms: 0.04033
  type: 'test'
  ...
# Subtest: runs of mixed separators collapse to one hyphen
ok 5 - runs of mixed separators collapse to one hyphen
  ---
  duration_ms: 0.08898
  type: 'test'
  ...
# Subtest: leading and trailing junk is trimmed
ok 6 - leading and trailing junk is trimmed
  ---
  duration_ms: 0.04547
  type: 'test'
  ...
# Subtest: all-separator input becomes empty string
ok 7 - all-separator input becomes empty string
  ---
  duration_ms: 0.04063
  type: 'test'
  ...
# Subtest: empty string stays empty
ok 8 - empty string stays empty
  ---
  duration_ms: 0.03626
  type: 'test'
  ...
# Subtest: non-ASCII letters act as separators
ok 9 - non-ASCII letters act as separators
  ---
  duration_ms: 0.05031
  type: 'test'
  ...
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 27.571509
Reasoning (chain-of-thought)
The user wants a JavaScript function `slugify` that converts a title string into a URL slug.
Requirements:
- Lowercase the whole string.
- Treat any character that is not an ASCII letter (`a-z`) or digit (`0-9`) as a separator.
- Replace each run of one or more separators with a single hyphen (`-`).
- Strip leading/trailing hyphens.
- Return the resulting slug. Empty string if all separators.

Let's break down the steps:
1. Convert input to lowercase.
2. Replace non-alphanumeric ASCII characters with a hyphen.
3. Replace multiple consecutive hyphens with a single hyphen.
4. Trim leading and trailing hyphens.

Regex approach:
`input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')`

Let's test this mentally with examples:
`"Hello World"` -> `"hello world"` -> `"hello-world"` -> `"hello-world"` ✓
`"  Hello,  World!  "` -> `"  hello,  world!  "` -> `"-hello--world-"` -> `"hello-world"` ✓
`"already-clean"` -> `"already-clean"` -> `"already-clean"` -> `"already-clean"` ✓
`"Top 10 Tips"` -> `"top 10 tips"` -> `"top-10-tips"` -> `"top-10-tips"` ✓
`"a---b__c"` -> `"a---b__c"` -> `"a-b-c"` -> `"a-b-c"` ✓
`"@#$%"` -> `"@#$%"` -> `"-"` -> `""` ✓

Wait, what about non-ASCII letters? The prompt says: "Treat any character that is not an ASCII letter (a–z) or digit (0–9) as a separator — this includes spaces, punctuation, underscores, and non-ASCII letters."
So `[^a-z0-9]` correctly matches non-ASCII letters as separators.

Let's double check the regex: `/[^a-z0-9]+/g`
This matches one or more characters that are not `a-z` or `0-9`.
Replacing with `-` works.
Then `/^-+|-+$/g` removes leading/trailing hyphens.

Is there any edge case?
Empty string input: `""` -> `""` -> `""` -> `""` ✓
String with only valid chars: `"abc"` -> `"abc"` -> `"abc"` -> `"abc"` ✓
String with mixed: `"café"` -> `"caf\u00e9"` -> `"caf-"` -> `"caf"` (since é is non-ASCII, it becomes separator, then hyphen, then trimmed). The prompt says non-ASCII letters are separators, so this is correct.

One minor thing: `input` might not be a string? The prompt says "Convert a human title string", so we can assume string input. But to be safe, maybe `String(input).toLowerCase()...` but not strictly necessary.

Let's write out the function:
```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```

Wait, does `/[^a-z0-9]+/g` handle newlines correctly? Yes, `.` doesn't match newlines by default, but `[^a-z0-9]` matches any character except a-z and 0-9, including newlines. So it's fine.

Let's test `"a---b__c"`:
`"a---b__c".toLowerCase()` -> `"a---b__c"`
`.replace(/[^a-z0-9]+/g, '-')` -> `"a-b-c"`
`.replace(/^-+|-+$/g, '')` -> `"a-b-c"`
Correct.

Test `"@#$%"`:
`"@#$%".toLowerCase()` -> `"@#$%"`
`.replace(/[^a-z0-9]+/g, '-')` -> `"-"`
`.replace(/^-+|-+$/g, '')` -> `""`
Correct.

Is there any case where `input` contains unicode characters that might be normalized? The prompt doesn't ask for unicode normalization, just treat non-ASCII as separators. So direct regex is fine.

One thing: `input` might be undefined or null? The prompt says "Convert a human title string", so we can assume valid string. I'll stick to the simple implementation.

Could also combine into one regex? Not easily without lookarounds, but two replaces is fine and readable.

Let's verify the exact signature requested:
```js
export function slugify(input) { /* ... */ }
```

Code:
```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```

Wait, should I handle cases where `input` is not a string? Usually in these challenges, input is guaranteed to be a string. I'll assume string.

Let's consider if `input` could be something else. I'll just use `String(input)` to be safe, or just `input.toLowerCase()`. The prompt says "Convert a human title string", so `input` is a string.

One minor point: `[^a-z0-9]` in JS regex matches any character that is not a-z or 0-9. This includes uppercase letters before `.toLowerCase()`, but since we call `.toLowerCase()` first, it's fine. Actually, if we call `.toLowerCase()` first, then `[^a-z0-9]` correctly matches everything else.

What about performance? It's fine.

Let's double check the exact wording: "Treat any character that is not an ASCII letter (a–z) or digit (0–9) as a separator".
So `[^a-z0-9]` is exactly right.

One potential issue: `input.toLowerCase()` might throw if `input` is null/undefined. I'll add a fallback or just assume string. I'll stick to assuming string as per typical coding challenge conventions.

Final code:
```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```

Wait, should I consider `input` might be passed as something else? I'll just use `String(input)` to be robust, but it's probably unnecessary. I'll keep it simple.

Let's test locally in mind:
`slugify("Hello World")` -> `"hello-world"`
`slugify("  Hello,  World!  ")` -> `"hello-world"`
`slugify("already-clean")` -> `"already-clean"`
`slugify("Top 10 Tips")` -> `"top-10-tips"`
`slugify("a---b__c")` -> `"a-b-c"`
`slugify("@#$%")` -> `""`

All match examples.

One detail: `replace(/^-+|-+$/g, '')` can be simplified to `replace(/^-+|-+$/g, '')` or just `replace(/^-+|-+$/g, '')`. It's fine.
Alternatively, `replace(/^-+|-+$/g, '')` works.
Could also use `trim('-')` but that's not standard JS. So regex is best.

Another option: `input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')`

Is there any case where `input` contains characters that become hyphens and then we need to trim? Yes, handled.

What about `input` being empty string? `"".toLowerCase()` -> `""`, replace -> `""`, replace -> `""`. Correct.

I think this is solid.

One minor consideration: The prompt says "ES module solution.js exporting a single function". So the file should just contain the export.

I will produce the exact code.

```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```

Wait, should I handle `input` possibly being non-st
…[431 chars elided]…
 check of the prompt: "Treat any character that is not an ASCII letter (a–z) or digit (0–9) as a separator".
Regex `/[^a-z0-9]+/g` does exactly that.
"Replace each run of one or more separators with a single hyphen (-)." -> `.replace(/[^a-z0-9]+/g, '-')`
"Strip any leading or trailing hyphens from the result." -> `.replace(/^-+|-+$/g, '')`
"Return the resulting slug (a string). An input made entirely of separators returns ""." -> handled.

Looks perfect.
I will generate the response. 
One minor point: `input.toLowerCase()` might fail if `input` is not a string. I'll assume valid input per spec. If I want to be super safe: `String(input).toLowerCase()...` but it's fine.

I'll output exactly the requested module.
Done. 
Let's produce the code. 
```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```
That's it.
All constraints met.
No extra fluff.
Proceeds. 
Self-Correction/Verification during thought:
Wait, does `[^a-z0-9]` match uppercase letters? Yes, but we call `.toLowerCase()` first, so it's fine.
What about non-ASCII letters like `é`? They are not in `a-z`, so they match `[^a-z0-9]`, become `-`, then trimmed if at ends, or become separators. This matches "non-ASCII letters" being separators.
All good.
Output matches request.
Done. 
I will format it as requested.
```js
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```
Ready. 
Output matches. 
Proceeds. 
[Final Check]
The prompt asks for `solution.js` exporting `slugify`.
Code:
```javascript
export function slugify(input) {
  return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}
```
Matches perfectly. No extra text.
Done. 
Output Generation. 
(Self-Correction/Refinement during thought)
Is there any chance `input` could be `null` or `undefined`? The prompt says "Convert a human title string", so it's safe.
I'll stick to the simple version.
All good. 
Proceeds. 
[Output Generation] -> *Proceeds*