Skip to content

Commit 28ade37

Browse files
committed
fix(utils): added title and sentence case functions
1 parent 15c4677 commit 28ade37

File tree

5 files changed

+274
-206
lines changed

5 files changed

+274
-206
lines changed

packages/utils/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export {
100100
kebabCaseToSentence,
101101
kebabCaseToCamelCase,
102102
formatToSentenceCase,
103-
} from './lib/string.js';
103+
} from './lib/case-conversions.js';
104104
export * from './lib/text-formats/index.js';
105105
export {
106106
capitalize,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { CamelCaseToKebabCase } from './types.js';
2+
3+
/**
4+
* Converts a kebab-case string to camelCase.
5+
* @param string - The kebab-case string to convert.
6+
* @returns The camelCase string.
7+
*/
8+
export function kebabCaseToCamelCase(string: string) {
9+
return string
10+
.split('-')
11+
.map((segment, index) =>
12+
index === 0
13+
? segment
14+
: segment.charAt(0).toUpperCase() + segment.slice(1),
15+
)
16+
.join('');
17+
}
18+
19+
/**
20+
* Converts a camelCase string to kebab-case.
21+
* @param string - The camelCase string to convert.
22+
* @returns The kebab-case string.
23+
*/
24+
export function camelCaseToKebabCase<T extends string>(
25+
string: T,
26+
): CamelCaseToKebabCase<T> {
27+
return string
28+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
29+
.replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
30+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
31+
.replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
32+
.toLowerCase() as CamelCaseToKebabCase<T>;
33+
}
34+
35+
/**
36+
* Converts a string to Title Case.
37+
* - Capitalizes the first letter of each major word.
38+
* - Keeps articles, conjunctions, and short prepositions in lowercase unless they are the first word.
39+
*
40+
* @param input - The string to convert.
41+
* @returns The formatted title case string.
42+
*/
43+
export function toTitleCase(input: string): string {
44+
const minorWords = new Set([
45+
'a',
46+
'an',
47+
'the',
48+
'and',
49+
'or',
50+
'but',
51+
'for',
52+
'nor',
53+
'on',
54+
'in',
55+
'at',
56+
'to',
57+
'by',
58+
'of',
59+
]);
60+
61+
return input
62+
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split PascalCase & camelCase
63+
.replace(/[_-]/g, ' ') // Replace kebab-case and snake_case with spaces
64+
.replace(/(\d+)/g, ' $1 ') // Add spaces around numbers
65+
.replace(/\s+/g, ' ') // Remove extra spaces
66+
.trim()
67+
.split(' ')
68+
.map((word, index) => {
69+
// Preserve uppercase acronyms (e.g., API, HTTP)
70+
if (/^[A-Z]{2,}$/.test(word)) {
71+
return word;
72+
}
73+
74+
// Capitalize first word or non-minor words
75+
if (index === 0 || !minorWords.has(word.toLowerCase())) {
76+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
77+
}
78+
return word.toLowerCase();
79+
})
80+
.join(' ');
81+
}
82+
83+
/**
84+
* Converts a string to Sentence Case.
85+
* - Capitalizes only the first letter of the sentence.
86+
* - Retains case of proper nouns.
87+
*
88+
* @param input - The string to convert.
89+
* @returns The formatted sentence case string.
90+
*/
91+
export function toSentenceCase(input: string): string {
92+
return input
93+
.replace(/([a-z])([A-Z])/g, '$1 $2') // Split PascalCase & camelCase
94+
.replace(/[_-]/g, ' ') // Replace kebab-case and snake_case with spaces
95+
.replace(/(\d+)/g, ' $1 ') // Add spaces around numbers
96+
.replace(/\s+/g, ' ') // Remove extra spaces
97+
.trim()
98+
.toLowerCase()
99+
.replace(/^(\w)/, match => match.toUpperCase()) // Capitalize first letter
100+
.replace(/\b([A-Z]{2,})\b/g, match => match); // Preserve uppercase acronyms
101+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
camelCaseToKebabCase,
3+
kebabCaseToCamelCase,
4+
toSentenceCase,
5+
toTitleCase,
6+
} from './case-conversions.js';
7+
8+
describe('kebabCaseToCamelCase', () => {
9+
it('should convert simple kebab-case to camelCase', () => {
10+
expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld');
11+
});
12+
13+
it('should handle multiple hyphens', () => {
14+
expect(kebabCaseToCamelCase('this-is-a-long-string')).toBe(
15+
'thisIsALongString',
16+
);
17+
});
18+
19+
it('should preserve numbers', () => {
20+
expect(kebabCaseToCamelCase('user-123-test')).toBe('user123Test');
21+
});
22+
23+
it('should handle single word', () => {
24+
expect(kebabCaseToCamelCase('hello')).toBe('hello');
25+
});
26+
27+
it('should handle empty string', () => {
28+
expect(kebabCaseToCamelCase('')).toBe('');
29+
});
30+
});
31+
32+
describe('camelCaseToKebabCase', () => {
33+
it('should convert simple camelCase to kebab-case', () => {
34+
expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world');
35+
});
36+
37+
it('should handle multiple capital letters', () => {
38+
expect(camelCaseToKebabCase('thisIsALongString')).toBe(
39+
'this-is-a-long-string',
40+
);
41+
});
42+
43+
it('should handle consecutive capital letters', () => {
44+
expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser');
45+
});
46+
47+
it('should handle spaces and underscores', () => {
48+
expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test');
49+
});
50+
51+
it('should handle single word', () => {
52+
expect(camelCaseToKebabCase('hello')).toBe('hello');
53+
});
54+
55+
it('should handle empty string', () => {
56+
expect(camelCaseToKebabCase('')).toBe('');
57+
});
58+
});
59+
60+
describe('toTitleCase', () => {
61+
it('should capitalize each word in a simple sentence', () => {
62+
expect(toTitleCase('hello world')).toBe('Hello World');
63+
});
64+
65+
it('should capitalize each word in a longer sentence', () => {
66+
expect(toTitleCase('this is a title')).toBe('This Is a Title');
67+
});
68+
69+
it('should convert PascalCase to title case', () => {
70+
expect(toTitleCase('FormatToTitleCase')).toBe('Format to Title Case');
71+
});
72+
73+
it('should convert camelCase to title case', () => {
74+
expect(toTitleCase('thisIsTest')).toBe('This Is Test');
75+
});
76+
77+
it('should convert kebab-case to title case', () => {
78+
expect(toTitleCase('hello-world-example')).toBe('Hello World Example');
79+
});
80+
81+
it('should convert snake_case to title case', () => {
82+
expect(toTitleCase('snake_case_example')).toBe('Snake Case Example');
83+
});
84+
85+
it('should capitalize a single word', () => {
86+
expect(toTitleCase('hello')).toBe('Hello');
87+
});
88+
89+
it('should handle numbers in words correctly', () => {
90+
expect(toTitleCase('chapter1Introduction')).toBe('Chapter 1 Introduction');
91+
});
92+
93+
it('should handle numbers in slugs correctly', () => {
94+
expect(toTitleCase('version2Release')).toBe('Version 2 Release');
95+
});
96+
97+
it('should handle acronyms properly', () => {
98+
expect(toTitleCase('apiResponse')).toBe('Api Response');
99+
});
100+
101+
it('should handle mixed-case inputs correctly', () => {
102+
expect(toTitleCase('thisIs-mixed_CASE')).toBe('This Is Mixed CASE');
103+
});
104+
105+
it('should not modify already formatted title case text', () => {
106+
expect(toTitleCase('Hello World')).toBe('Hello World');
107+
});
108+
109+
it('should return an empty string when given an empty input', () => {
110+
expect(toTitleCase('')).toBe('');
111+
});
112+
});
113+
114+
describe('toSentenceCase', () => {
115+
it('should convert a simple sentence to sentence case', () => {
116+
expect(toSentenceCase('hello world')).toBe('Hello world');
117+
});
118+
119+
it('should maintain a correctly formatted sentence', () => {
120+
expect(toSentenceCase('This is a test')).toBe('This is a test');
121+
});
122+
123+
it('should convert PascalCase to sentence case', () => {
124+
expect(toSentenceCase('FormatToSentenceCase')).toBe(
125+
'Format to sentence case',
126+
);
127+
});
128+
129+
it('should convert camelCase to sentence case', () => {
130+
expect(toSentenceCase('thisIsTest')).toBe('This is test');
131+
});
132+
133+
it('should convert kebab-case to sentence case', () => {
134+
expect(toSentenceCase('hello-world-example')).toBe('Hello world example');
135+
});
136+
137+
it('should convert snake_case to sentence case', () => {
138+
expect(toSentenceCase('snake_case_example')).toBe('Snake case example');
139+
});
140+
141+
it('should capitalize a single word', () => {
142+
expect(toSentenceCase('hello')).toBe('Hello');
143+
});
144+
145+
it('should handle numbers in words correctly', () => {
146+
expect(toSentenceCase('chapter1Introduction')).toBe(
147+
'Chapter 1 introduction',
148+
);
149+
});
150+
151+
it('should handle numbers in slugs correctly', () => {
152+
expect(toSentenceCase('version2Release')).toBe('Version 2 release');
153+
});
154+
155+
it('should handle acronyms properly', () => {
156+
expect(toSentenceCase('apiResponse')).toBe('Api response');
157+
});
158+
159+
it('should handle mixed-case inputs correctly', () => {
160+
expect(toSentenceCase('thisIs-mixed_CASE')).toBe('This is mixed case');
161+
});
162+
163+
it('should not modify already formatted sentence case text', () => {
164+
expect(toSentenceCase('This is a normal sentence.')).toBe(
165+
'This is a normal sentence.',
166+
);
167+
});
168+
169+
it('should return an empty string when given an empty input', () => {
170+
expect(toSentenceCase('')).toBe('');
171+
});
172+
});

packages/utils/src/lib/string.ts

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)