Skip to content

Commit 5cda7f7

Browse files
committed
🐛 with toSlugCase(), toTitleCase(), toWords()
1 parent c14fc3e commit 5cda7f7

File tree

4 files changed

+126
-16
lines changed

4 files changed

+126
-16
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.2",
44
"license": "AGPL-3.0",
55
"exports": "./index.ts",
66
"exclude": [".github/"]

index.test.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ import {
4444
longestCommonPrefix,
4545
longestCommonSuffix,
4646
longestUncommonInfixes,
47+
toWords,
48+
toTitleCase,
4749
toKebabCase,
50+
toSlugCase,
4851
toSuperscript,
4952
tverskyDistance,
5053
} from "./index.ts";
@@ -1225,17 +1228,67 @@ Deno.test("longestUncommonInfixes", () => {
12251228

12261229

12271230

1231+
Deno.test("toWords", () => {
1232+
const a = toWords("Malwa Plateau");
1233+
assertEquals(a, ["Malwa", "Plateau"]);
1234+
const b = toWords("::chota::nagpur::");
1235+
assertEquals(b, ["chota", "nagpur"]);
1236+
const c = toWords("deccan___plateau");
1237+
assertEquals(c, ["deccan", "plateau"]);
1238+
const d = toWords("westernGhats");
1239+
assertEquals(d, ["western", "Ghats"]);
1240+
const e = toWords("parseURLToJSON");
1241+
assertEquals(e, ["parse", "URL", "To", "JSON"]);
1242+
});
1243+
1244+
1245+
1246+
1247+
Deno.test("toTitleCase", () => {
1248+
const a = toTitleCase("Geminid meteor shower");
1249+
assertEquals(a, "Geminid Meteor Shower");
1250+
const b = toTitleCase("deccan___plateau");
1251+
assertEquals(b, "Deccan Plateau");
1252+
const c = toTitleCase("parseURLToJSON", null, "_");
1253+
assertEquals(c, "Parse_Url_To_Json");
1254+
});
1255+
1256+
1257+
1258+
12281259
Deno.test("toKebabCase", () => {
12291260
const a = toKebabCase("Malwa Plateau");
12301261
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");
1262+
const b = toKebabCase("malwaPlateau");
1263+
assertEquals(b, "malwa-plateau");
1264+
const c = toKebabCase("::chota::nagpur::", null, "_");
1265+
assertEquals(c, "chota_nagpur");
1266+
const d = toKebabCase("deccan___plateau", /_+/g, ".");
1267+
assertEquals(d, "deccan.plateau");
1268+
const e = toKebabCase("Some text_with-mixed CASE");
1269+
assertEquals(e, "some-text-with-mixed-case");
1270+
const f = toKebabCase("someTextWithMixedCase");
1271+
assertEquals(f, "some-text-with-mixed-case");
1272+
const g = toKebabCase("IAmListeningToFMWhileLoadingDifferentURL");
1273+
assertEquals(g, "i-am-listening-to-fm-while-loading-different-url");
1274+
// const h = toKebabCase("I can't believe it's not butter!");
1275+
// assertEquals(h, "i-cant-believe-its-not-butter");
1276+
});
1277+
1278+
1279+
1280+
1281+
Deno.test("toSlugCase", () => {
1282+
const a = toSlugCase("Malwa Plateau");
1283+
assertEquals(a, "malwa-plateau");
1284+
const b = toSlugCase("malwaPlateau");
1285+
assertEquals(b, "malwa-plateau");
1286+
const c = toSlugCase("Curaçao São Tomé & Príncipe!");
1287+
assertEquals(c, "curacao-sao-tome-principe");
1288+
const d = toSlugCase("你好世界 hello world");
1289+
assertEquals(d, "hello-world");
1290+
// const e = toSlugCase("Æther & Œuvre — résumé", null, "_");
1291+
// assertEquals(e, "aether_oeuvre_resume");
12391292
});
12401293

12411294

index.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,45 @@ function toBorderUpperCase(x: string): string {
13481348
}
13491349

13501350

1351+
/**
1352+
* Split a string into words, after de-casing it.
1353+
* @param x a string
1354+
* @param re word seperator pattern [/[^0-9A-Za-z]+/g]
1355+
* @returns words in the string
1356+
* @example
1357+
* ```javascript
1358+
* xstring.toWords('Malwa Plateau');
1359+
* // → ['Malwa', 'Plateau']
1360+
*
1361+
* xstring.toWords('::chota::nagpur::');
1362+
* // → ['chota', 'nagpur']
1363+
*
1364+
* xstring.toWords('deccan___plateau');
1365+
* // → ['deccan', 'plateau']
1366+
*
1367+
* xstring.toWords('westernGhats');
1368+
* // → ['western', 'Ghats']
1369+
*
1370+
* xstring.toWords('parseURLToJSON');
1371+
* // → ['parse', 'URL', 'To', 'JSON']
1372+
*/
1373+
export function toWords(x: string, re: RegExp | null=null): string[] {
1374+
const words = [];
1375+
const tokens = x.split(re || /[^0-9A-Za-z]+/g).filter(IDENTITY);
1376+
for (const token of tokens) {
1377+
const re = /[A-Z]+/g;
1378+
let m = null, i = 0;
1379+
while ((m = re.exec(token)) != null) {
1380+
if (i!==m.index) words.push(token.slice(i, m.index));
1381+
if (m[0].length===1 || m.index + m[0].length === token.length) i = m.index;
1382+
else { words.push(token.slice(m.index, m.index + m[0].length - 1)); i = m.index + m[0].length - 1; }
1383+
}
1384+
if (i!==token.length) words.push(token.slice(i));
1385+
}
1386+
return words;
1387+
}
1388+
1389+
13511390
/**
13521391
* Convert a string to title-case.
13531392
* @param x a string
@@ -1358,16 +1397,15 @@ function toBorderUpperCase(x: string): string {
13581397
* xstring.toTitleCase('Geminid meteor shower');
13591398
* // → 'Geminid Meteor Shower'
13601399
*
1361-
* xstring.toTitleCase('geminid-meteor-shower');
1362-
* // → 'Geminid Meteor Shower'
1400+
* xstring.toTitleCase('deccan___plateau');
1401+
* // → 'Deccan Plateau'
13631402
*
1364-
* xstring.toTitleCase('geminid_meteor_shower', '_');
1365-
* // → 'Geminid Meteor Shower'
1403+
* xstring.toTitleCase('parseURLToJSON', null, '_');
1404+
* // → 'Parse_URL_To_JSON'
13661405
* ```
13671406
*/
1368-
function _toTitleCase(x: string, re: RegExp | null=null): string {
1369-
const words = x.split(re || /[^0-9A-Za-z]+/g).filter(IDENTITY);
1370-
return words.map(toBeginUpperCase).join(" ");
1407+
export function toTitleCase(x: string, re: RegExp | null=null, sep=" "): string {
1408+
return toWords(x, re).map(toBeginUpperCase).join(sep);
13711409
}
13721410

13731411

@@ -1445,6 +1483,21 @@ export function toCamelCase(x: string, re: RegExp | null=null, upper: boolean=fa
14451483
export function toPascalCase(x: string, re: RegExp | null=null): string {
14461484
return toCamelCase(x, re, true);
14471485
}
1486+
1487+
1488+
/**
1489+
* Convert a string to slug-case (URL-friendly kebab-case).
1490+
* @param x a string
1491+
* @param re word separator pattern [/[^0-9A-Za-z]+/g]
1492+
* @param sep separator to join with [-]
1493+
* @returns slug-case | slug<join>case
1494+
*/
1495+
export function toSlugCase(x: string, re: RegExp | null = null, sep: string = "-"): string {
1496+
x = x.normalize("NFKD").replace(/[\u0300-\u036f]/g, ""); // Remove accents
1497+
// deno-lint-ignore no-control-regex
1498+
return toKebabCase(x.replace(/[^\x00-\x7F]/g, ""), re, sep); // Remove non-ASCII chars
1499+
}
1500+
export {toSlugCase as slugify};
14481501
//#endregion
14491502

14501503

0 commit comments

Comments
 (0)