Skip to content

Commit 94357aa

Browse files
committed
🐛 with toSlugCase(), toWords()
1 parent c14fc3e commit 94357aa

File tree

4 files changed

+107
-9
lines changed

4 files changed

+107
-9
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ xstring.tverskyDistance('pikachu', 'raichu', 3, 0.2, 0.4);
4848
| [toSnakeCase] | Convert a string to snake-case. |
4949
| [toCamelCase] | Convert a string to camel-case. |
5050
| [toPascalCase] | Convert a string to pascal-case. |
51+
| [toSlugCase] | Convert a string to slug-case (URL-friendly kebab-case). |
52+
| [toWords] | Split a string into words, after de-casing it. |
5153
| | |
5254
| [toBaseline] | Convert a string to baseline characters (limited support). |
5355
| [toSuperscript] | Convert a string to superscript characters (limited support). |
@@ -274,6 +276,8 @@ As of 26 June 2025, this project is licensed under AGPL-3.0. Previous versions r
274276
[toSnakeCase]: https://jsr.io/@nodef/extra-string/doc/~/toSnakeCase
275277
[toCamelCase]: https://jsr.io/@nodef/extra-string/doc/~/toCamelCase
276278
[toPascalCase]: https://jsr.io/@nodef/extra-string/doc/~/toPascalCase
279+
[toSlugCase]: https://jsr.io/@nodef/extra-string/doc/~/toSlugCase
280+
[toWords]: https://jsr.io/@nodef/extra-string/doc/~/toWords
277281
[ngrams]: https://jsr.io/@nodef/extra-string/doc/~/ngrams
278282
[uniqueNgrams]: https://jsr.io/@nodef/extra-string/doc/~/uniqueNgrams
279283
[countNgrams]: https://jsr.io/@nodef/extra-string/doc/~/countNgrams

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nodef/extra-string",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"license": "AGPL-3.0",
55
"exports": "./index.ts",
66
"exclude": [".github/"]

index.test.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ import {
4444
longestCommonPrefix,
4545
longestCommonSuffix,
4646
longestUncommonInfixes,
47+
toWords,
4748
toKebabCase,
49+
toSlugCase,
4850
toSuperscript,
4951
tverskyDistance,
5052
} from "./index.ts";
@@ -1225,17 +1227,55 @@ Deno.test("longestUncommonInfixes", () => {
12251227

12261228

12271229

1230+
Deno.test("toWords", () => {
1231+
const a = toWords("Malwa Plateau");
1232+
assertEquals(a, ["Malwa", "Plateau"]);
1233+
const b = toWords("::chota::nagpur::");
1234+
assertEquals(b, ["chota", "nagpur"]);
1235+
const c = toWords("deccan___plateau");
1236+
assertEquals(c, ["deccan", "plateau"]);
1237+
const d = toWords("westernGhats");
1238+
assertEquals(d, ["western", "Ghats"]);
1239+
const e = toWords("parseURLToJSON");
1240+
assertEquals(e, ["parse", "URL", "To", "JSON"]);
1241+
});
1242+
1243+
1244+
1245+
12281246
Deno.test("toKebabCase", () => {
12291247
const a = toKebabCase("Malwa Plateau");
12301248
assertEquals(a, "malwa-plateau");
1231-
const b = toKebabCase("::chota::nagpur::", null, "_");
1232-
assertEquals(b, "chota_nagpur");
1233-
const c = toKebabCase("deccan___plateau", /_+/g, ".");
1234-
assertEquals(c, "deccan.plateau");
1235-
const d = toKebabCase("Some text_with-mixed CASE");
1236-
assertEquals(d, "some-text-with-mixed-case");
1237-
const e = toKebabCase("IAmListeningToFMWhileLoadingDifferentURL");
1238-
assertEquals(e, "i-am-listening-to-fm-while-loading-different-url");
1249+
const b = toKebabCase("malwaPlateau");
1250+
assertEquals(b, "malwa-plateau");
1251+
const c = toKebabCase("::chota::nagpur::", null, "_");
1252+
assertEquals(c, "chota_nagpur");
1253+
const d = toKebabCase("deccan___plateau", /_+/g, ".");
1254+
assertEquals(d, "deccan.plateau");
1255+
const e = toKebabCase("Some text_with-mixed CASE");
1256+
assertEquals(e, "some-text-with-mixed-case");
1257+
const f = toKebabCase("someTextWithMixedCase");
1258+
assertEquals(f, "some-text-with-mixed-case");
1259+
const g = toKebabCase("IAmListeningToFMWhileLoadingDifferentURL");
1260+
assertEquals(g, "i-am-listening-to-fm-while-loading-different-url");
1261+
// const h = toKebabCase("I can't believe it's not butter!");
1262+
// assertEquals(h, "i-cant-believe-its-not-butter");
1263+
});
1264+
1265+
1266+
1267+
1268+
Deno.test("toSlugCase", () => {
1269+
const a = toSlugCase("Malwa Plateau");
1270+
assertEquals(a, "malwa-plateau");
1271+
const b = toSlugCase("malwaPlateau");
1272+
assertEquals(b, "malwa-plateau");
1273+
const c = toSlugCase("Curaçao São Tomé & Príncipe!");
1274+
assertEquals(c, "curacao-sao-tome-principe");
1275+
const d = toSlugCase("你好世界 hello world");
1276+
assertEquals(d, "hello-world");
1277+
// const e = toSlugCase("Æther & Œuvre — résumé", null, "_");
1278+
// assertEquals(e, "aether_oeuvre_resume");
12391279
});
12401280

12411281

index.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,45 @@ function _toTitleCase(x: string, re: RegExp | null=null): string {
13711371
}
13721372

13731373

1374+
/**
1375+
* Split a string into words, after de-casing it.
1376+
* @param x a string
1377+
* @param re word seperator pattern [/[^0-9A-Za-z]+/g]
1378+
* @returns words in the string
1379+
* @example
1380+
* ```javascript
1381+
* xstring.toWords('Malwa Plateau');
1382+
* // → ['Malwa', 'Plateau']
1383+
*
1384+
* xstring.toWords('::chota::nagpur::');
1385+
* // → ['chota', 'nagpur']
1386+
*
1387+
* xstring.toWords('deccan___plateau');
1388+
* // → ['deccan', 'plateau']
1389+
*
1390+
* xstring.toWords('westernGhats');
1391+
* // → ['western', 'Ghats']
1392+
*
1393+
* xstring.toWords('parseURLToJSON');
1394+
* // → ['parse', 'URL', 'To', 'JSON']
1395+
*/
1396+
export function toWords(x: string, re: RegExp | null=null): string[] {
1397+
const words = [];
1398+
const tokens = x.split(re || /[^0-9A-Za-z]+/g).filter(IDENTITY);
1399+
for (const token of tokens) {
1400+
const re = /[A-Z]+/g;
1401+
let m = null, i = 0;
1402+
while ((m = re.exec(token)) != null) {
1403+
if (i!==m.index) words.push(token.slice(i, m.index));
1404+
if (m[0].length===1 || m.index + m[0].length === token.length) i = m.index;
1405+
else { words.push(token.slice(m.index, m.index + m[0].length - 1)); i = m.index + m[0].length - 1; }
1406+
}
1407+
if (i!==token.length) words.push(token.slice(i));
1408+
}
1409+
return words;
1410+
}
1411+
1412+
13741413
/**
13751414
* Convert a string to kebab-case.
13761415
* @param x a string
@@ -1445,6 +1484,21 @@ export function toCamelCase(x: string, re: RegExp | null=null, upper: boolean=fa
14451484
export function toPascalCase(x: string, re: RegExp | null=null): string {
14461485
return toCamelCase(x, re, true);
14471486
}
1487+
1488+
1489+
/**
1490+
* Convert a string to slug-case (URL-friendly kebab-case).
1491+
* @param x a string
1492+
* @param re word separator pattern [/[^0-9A-Za-z]+/g]
1493+
* @param sep separator to join with [-]
1494+
* @returns slug-case | slug<join>case
1495+
*/
1496+
export function toSlugCase(x: string, re: RegExp | null = null, sep: string = "-"): string {
1497+
x = x.normalize("NFKD").replace(/[\u0300-\u036f]/g, ""); // Remove accents
1498+
// deno-lint-ignore no-control-regex
1499+
return toKebabCase(x.replace(/[^\x00-\x7F]/g, ""), re, sep); // Remove non-ASCII chars
1500+
}
1501+
export {toSlugCase as slugify};
14481502
//#endregion
14491503

14501504

0 commit comments

Comments
 (0)