Unicode gives every character a number, called its code point, written like U+00E9. Some characters can be stored in more than one way. é can be one code point, U+00E9, a precomposed e with an acute accent. Or it can be two: a plain e, U+0065, followed by U+0301, the combining acute accent. Both show as the same é, but they're different strings, so === says they differ. Unicode normalization converts text to one standard form, so that text that looks the same is stored the same, and comparing it works.
A combining mark is a code point that isn't a character on its own: it attaches to the character before it, adding an accent, a dot, a tilde, or a cedilla. Marks can stack. The Vietnamese ệ is an e with a dot below and a circumflex, so it can be stored as anywhere from one code point to three. Like any code point, each mark counts in length.
const composed = "\u00E9"; // é, one code point
const decomposed = "e\u0301"; // é, an e and a combining accent
composed === decomposed; // false
composed.length; // 1
decomposed.length; // 2
composed.normalize("NFD") === decomposed; // true: NFD splits é into e and U+0301
decomposed.normalize("NFC") === composed; // true: NFC joins them into U+00E9
composed.normalize() === decomposed.normalize(); // true: both become NFC, the default
| Form | What it does | Example |
|---|---|---|
| NFC | Joins letters and the marks after them into precomposed code points, where it can | e and U+0301 become é, U+00E9 |
| NFD | Splits each precomposed letter into its base letter followed by its combining marks | é, U+00E9, becomes e and U+0301 |
| NFKC | Like NFC, and also replaces compatibility characters with their plain versions | fi becomes fi, ² becomes 2, and A becomes A |
| NFKD | Like NFD, with the same replacements as NFKC | fi becomes fi, and é becomes e and U+0301 |
NF stands for normalization form, C for composition, D for decomposition, and K for compatibility. NFC and NFD only switch between ways of storing the same characters. The K forms go further: they replace characters Unicode keeps only for compatibility with older systems, such as the ligature fi (an f and an i as one character), superscripts like ², and full-width letters like A, with their plain versions. That can change the meaning, since x² becomes x2, so the K forms suit matching and search, not storing what someone wrote.
Text reaches your code in either form, depending on the keyboard, the operating system, and the app it was typed or pasted in. So normalize before you compare or search. If someone searches for Café, typed composed, in text stored decomposed, "Cafe\u0301 au lait".includes("Café") is false. Normalize both sides to the same form and it's true. The same goes for checking whether a username is taken, matching tags, and using text as a key in a Map or an object. String.prototype.normalize(form) does it, with "NFC" as the default form, in every current browser and Node.
Write slugify(text). It turns a title into a slug, the readable part of a URL like /recipes/creme-brulee-in-10-minutes. A slug contains only the lowercase letters a to z, the digits 0 to 9, and hyphens.
Lowercase every letter and remove its accents: É becomes e, ñ becomes n, and ř becomes r. An accent may be stored in the letter itself or as a separate mark right after it (e followed by U+0301, the combining acute accent); remove it either way.
Drop apostrophes, both ' and ’, so Don't becomes dont. Every other run of characters that aren't a to z or 0 to 9 becomes a single hyphen: spaces, punctuation, symbols, emoji, and letters from other alphabets.
The slug never starts or ends with a hyphen. If nothing is left, return "".
slugify("Crème Brûlée in 10 Minutes!"); // "creme-brulee-in-10-minutes"
slugify(" Don't stop -- me now "); // "dont-stop-me-now"
slugify("São Paulo & Tōkyō"); // "sao-paulo-tokyo"
slugify("東京"); // ""
expect(slugify("Hello World")).toBe("hello-world");
expect(slugify("JavaScript Problems")).toBe("javascript-problems");
expect(slugify("Crème Brûlée")).toBe("creme-brulee");
expect(slugify("Dvořák")).toBe("dvorak");
expect(slugify("ÉCOLE Ñandú")).toBe("ecole-nandu");
expect(slugify("Zoë Ångström")).toBe("zoe-angstrom");
expect(slugify("Việt Nam")).toBe("viet-nam");
expect(slugify("Pe\u0301rez")).toBe("perez");
expect(slugify("Cafe\u0301 Mu\u0308ller")).toBe("cafe-muller");
expect(slugify("rock & roll")).toBe("rock-roll");
expect(slugify("one -- two__three...four")).toBe("one-two-three-four");
expect(slugify("I ❤️ JS")).toBe("i-js");
expect(slugify("I❤️JS")).toBe("i-js");
expect(slugify("Tokyo東京Tower")).toBe("tokyo-tower");
expect(slugify("C++/C# tips")).toBe("c-c-tips");
expect(slugify("Ελληνικά and English")).toBe("and-english");
expect(slugify("Don't stop")).toBe("dont-stop");
expect(slugify("It’s Ada’s book")).toBe("its-adas-book");
expect(slugify("rock 'n' roll")).toBe("rock-n-roll");
expect(slugify("Top 10 tips for 2024")).toBe("top-10-tips-for-2024");
expect(slugify("HTML5 vs. CSS3")).toBe("html5-vs-css3");
expect(slugify(" --Hello, world!-- ")).toBe("hello-world");
expect(slugify("¿Qué pasa?")).toBe("que-pasa");
expect(slugify("...dots")).toBe("dots");
expect(slugify("")).toBe("");
expect(slugify("!!! ???")).toBe("");
expect(slugify("東京")).toBe("");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Slugify. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.