Skip to content

Commit b61ebe7

Browse files
committed
Merge branch 'ref/komoji'
* ref/komoji: komoji
2 parents 9819e3a + 4031ca3 commit b61ebe7

File tree

3 files changed

+255
-4
lines changed

3 files changed

+255
-4
lines changed

packages/komoji/README.md

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,42 @@ toCamelCase('__private_field', true); // privateField
6060
toCamelCase('123-invalid', true); // invalid
6161
```
6262

63+
### Transform to snake_case
64+
65+
```typescript
66+
import { toSnakeCase } from 'komoji';
67+
68+
toSnakeCase('helloWorld'); // hello_world
69+
toSnakeCase('UserName'); // user_name
70+
toSnakeCase('api-response-data'); // api_response_data
71+
toSnakeCase('myComponentV2'); // my_component_v2
72+
toSnakeCase('HTTPSConnection'); // https_connection
73+
```
74+
75+
### Transform to kebab-case
76+
77+
```typescript
78+
import { toKebabCase } from 'komoji';
79+
80+
toKebabCase('helloWorld'); // hello-world
81+
toKebabCase('UserName'); // user-name
82+
toKebabCase('api_response_data'); // api-response-data
83+
toKebabCase('myComponentV2'); // my-component-v2
84+
toKebabCase('HTTPSConnection'); // https-connection
85+
```
86+
87+
### Transform to CONSTANT_CASE
88+
89+
```typescript
90+
import { toConstantCase } from 'komoji';
91+
92+
toConstantCase('helloWorld'); // HELLO_WORLD
93+
toConstantCase('UserName'); // USER_NAME
94+
toConstantCase('api-response-data'); // API_RESPONSE_DATA
95+
toConstantCase('myComponentV2'); // MY_COMPONENT_V2
96+
toConstantCase('HTTPSConnection'); // HTTPS_CONNECTION
97+
```
98+
6399
### Validate Identifiers
64100

65101
```typescript
@@ -79,25 +115,41 @@ isValidIdentifierCamelized('-invalid'); // false (starts with hyphen)
79115

80116
## API
81117

82-
### `toPascalCase(str: string): string`
118+
### Case Transformation Functions
119+
120+
#### `toPascalCase(str: string): string`
83121

84122
Converts a string to PascalCase by capitalizing the first letter of each word and removing separators.
85123

86124
**Supported separators:** hyphens (`-`), underscores (`_`), spaces (` `)
87125

88-
### `toCamelCase(key: string, stripLeadingNonAlphabetChars?: boolean): string`
126+
#### `toCamelCase(key: string, stripLeadingNonAlphabetChars?: boolean): string`
89127

90128
Converts a string to camelCase with an optional flag to strip leading non-alphabetic characters.
91129

92130
**Parameters:**
93131
- `key` - The string to transform
94132
- `stripLeadingNonAlphabetChars` - Remove leading non-alphabetic characters (default: `false`)
95133

96-
### `isValidIdentifier(key: string): boolean`
134+
#### `toSnakeCase(str: string): string`
135+
136+
Converts a string to snake_case. Handles camelCase, PascalCase, kebab-case, and space-separated strings. Properly inserts underscores between words and before numbers.
137+
138+
#### `toKebabCase(str: string): string`
139+
140+
Converts a string to kebab-case. Handles camelCase, PascalCase, snake_case, and space-separated strings. Properly inserts hyphens between words and before numbers.
141+
142+
#### `toConstantCase(str: string): string`
143+
144+
Converts a string to CONSTANT_CASE (also known as SCREAMING_SNAKE_CASE). Perfect for environment variables and constants. Handles all common case formats and properly separates words and numbers.
145+
146+
### Validation Functions
147+
148+
#### `isValidIdentifier(key: string): boolean`
97149

98150
Checks if a string is a valid JavaScript identifier (follows standard naming rules).
99151

100-
### `isValidIdentifierCamelized(key: string): boolean`
152+
#### `isValidIdentifierCamelized(key: string): boolean`
101153

102154
Checks if a string can be transformed into a valid JavaScript identifier (allows internal hyphens that will be removed during camelization).
103155

packages/komoji/__tests__/casing.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import {
33
isValidIdentifierCamelized,
44
toCamelCase,
55
toPascalCase,
6+
toSnakeCase,
7+
toKebabCase,
8+
toConstantCase,
69
} from '../src';
710

811
it('should convert strings to PascalCase', () => {
@@ -95,3 +98,145 @@ describe('toCamelCase', () => {
9598
expect(toCamelCase('hello___world--great')).toBe('helloWorldGreat');
9699
});
97100
});
101+
102+
describe('toSnakeCase', () => {
103+
test('converts camelCase to snake_case', () => {
104+
expect(toSnakeCase('helloWorld')).toBe('hello_world');
105+
});
106+
107+
test('converts PascalCase to snake_case', () => {
108+
expect(toSnakeCase('HelloWorld')).toBe('hello_world');
109+
});
110+
111+
test('converts kebab-case to snake_case', () => {
112+
expect(toSnakeCase('hello-world')).toBe('hello_world');
113+
});
114+
115+
test('converts space separated to snake_case', () => {
116+
expect(toSnakeCase('hello world')).toBe('hello_world');
117+
});
118+
119+
test('handles mixed case with numbers', () => {
120+
expect(toSnakeCase('version2APIKey')).toBe('version_2_api_key');
121+
});
122+
123+
test('handles already snake_case strings', () => {
124+
expect(toSnakeCase('hello_world')).toBe('hello_world');
125+
});
126+
127+
test('handles multiple separators together', () => {
128+
expect(toSnakeCase('hello___world--great')).toBe('hello_world_great');
129+
});
130+
131+
test('handles empty string', () => {
132+
expect(toSnakeCase('')).toBe('');
133+
});
134+
135+
test('removes leading and trailing underscores', () => {
136+
expect(toSnakeCase('_hello_world_')).toBe('hello_world');
137+
});
138+
139+
test('handles single word', () => {
140+
expect(toSnakeCase('hello')).toBe('hello');
141+
});
142+
143+
test('handles consecutive capitals', () => {
144+
expect(toSnakeCase('HTTPSConnection')).toBe('https_connection');
145+
});
146+
});
147+
148+
describe('toKebabCase', () => {
149+
test('converts camelCase to kebab-case', () => {
150+
expect(toKebabCase('helloWorld')).toBe('hello-world');
151+
});
152+
153+
test('converts PascalCase to kebab-case', () => {
154+
expect(toKebabCase('HelloWorld')).toBe('hello-world');
155+
});
156+
157+
test('converts snake_case to kebab-case', () => {
158+
expect(toKebabCase('hello_world')).toBe('hello-world');
159+
});
160+
161+
test('converts space separated to kebab-case', () => {
162+
expect(toKebabCase('hello world')).toBe('hello-world');
163+
});
164+
165+
test('handles mixed case with numbers', () => {
166+
expect(toKebabCase('version2APIKey')).toBe('version-2-api-key');
167+
});
168+
169+
test('handles already kebab-case strings', () => {
170+
expect(toKebabCase('hello-world')).toBe('hello-world');
171+
});
172+
173+
test('handles multiple separators together', () => {
174+
expect(toKebabCase('hello___world--great')).toBe('hello-world-great');
175+
});
176+
177+
test('handles empty string', () => {
178+
expect(toKebabCase('')).toBe('');
179+
});
180+
181+
test('removes leading and trailing hyphens', () => {
182+
expect(toKebabCase('-hello-world-')).toBe('hello-world');
183+
});
184+
185+
test('handles single word', () => {
186+
expect(toKebabCase('hello')).toBe('hello');
187+
});
188+
189+
test('handles consecutive capitals', () => {
190+
expect(toKebabCase('HTTPSConnection')).toBe('https-connection');
191+
});
192+
});
193+
194+
describe('toConstantCase', () => {
195+
test('converts camelCase to CONSTANT_CASE', () => {
196+
expect(toConstantCase('helloWorld')).toBe('HELLO_WORLD');
197+
});
198+
199+
test('converts PascalCase to CONSTANT_CASE', () => {
200+
expect(toConstantCase('HelloWorld')).toBe('HELLO_WORLD');
201+
});
202+
203+
test('converts kebab-case to CONSTANT_CASE', () => {
204+
expect(toConstantCase('hello-world')).toBe('HELLO_WORLD');
205+
});
206+
207+
test('converts snake_case to CONSTANT_CASE', () => {
208+
expect(toConstantCase('hello_world')).toBe('HELLO_WORLD');
209+
});
210+
211+
test('converts space separated to CONSTANT_CASE', () => {
212+
expect(toConstantCase('hello world')).toBe('HELLO_WORLD');
213+
});
214+
215+
test('handles mixed case with numbers', () => {
216+
expect(toConstantCase('version2APIKey')).toBe('VERSION_2_API_KEY');
217+
});
218+
219+
test('handles already CONSTANT_CASE strings', () => {
220+
expect(toConstantCase('HELLO_WORLD')).toBe('HELLO_WORLD');
221+
});
222+
223+
test('handles multiple separators together', () => {
224+
expect(toConstantCase('hello___world--great')).toBe('HELLO_WORLD_GREAT');
225+
});
226+
227+
test('handles empty string', () => {
228+
expect(toConstantCase('')).toBe('');
229+
});
230+
231+
test('removes leading and trailing underscores', () => {
232+
expect(toConstantCase('_hello_world_')).toBe('HELLO_WORLD');
233+
});
234+
235+
test('handles single word', () => {
236+
expect(toConstantCase('hello')).toBe('HELLO');
237+
});
238+
239+
test('handles consecutive capitals', () => {
240+
expect(toConstantCase('HTTPSConnection')).toBe('HTTPS_CONNECTION');
241+
});
242+
});

packages/komoji/src/index.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,57 @@ export function isValidIdentifierCamelized(key: string) {
3434
!/^-/.test(key)
3535
);
3636
}
37+
38+
export function toSnakeCase(str: string) {
39+
return str
40+
// Insert an underscore before the last capital in a sequence of capitals followed by a lowercase letter
41+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
42+
// Insert an underscore between lower and upper case letters
43+
.replace(/([a-z])([A-Z])/g, '$1_$2')
44+
// Insert an underscore between letters and numbers
45+
.replace(/([a-zA-Z])(\d)/g, '$1_$2')
46+
.replace(/(\d)([a-zA-Z])/g, '$1_$2')
47+
// Replace spaces, hyphens, and existing underscores with single underscore
48+
.replace(/[\s-]+/g, '_')
49+
// Remove multiple consecutive underscores
50+
.replace(/_+/g, '_')
51+
// Remove leading/trailing underscores and convert to lowercase
52+
.replace(/^_+|_+$/g, '')
53+
.toLowerCase();
54+
}
55+
56+
export function toKebabCase(str: string) {
57+
return str
58+
// Insert a hyphen before the last capital in a sequence of capitals followed by a lowercase letter
59+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
60+
// Insert a hyphen between lower and upper case letters
61+
.replace(/([a-z])([A-Z])/g, '$1-$2')
62+
// Insert a hyphen between letters and numbers
63+
.replace(/([a-zA-Z])(\d)/g, '$1-$2')
64+
.replace(/(\d)([a-zA-Z])/g, '$1-$2')
65+
// Replace spaces, underscores, and existing hyphens with single hyphen
66+
.replace(/[\s_]+/g, '-')
67+
// Remove multiple consecutive hyphens
68+
.replace(/-+/g, '-')
69+
// Remove leading/trailing hyphens and convert to lowercase
70+
.replace(/^-+|-+$/g, '')
71+
.toLowerCase();
72+
}
73+
74+
export function toConstantCase(str: string) {
75+
return str
76+
// Insert an underscore before the last capital in a sequence of capitals followed by a lowercase letter
77+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
78+
// Insert an underscore between lower and upper case letters
79+
.replace(/([a-z])([A-Z])/g, '$1_$2')
80+
// Insert an underscore between letters and numbers
81+
.replace(/([a-zA-Z])(\d)/g, '$1_$2')
82+
.replace(/(\d)([a-zA-Z])/g, '$1_$2')
83+
// Replace spaces, hyphens, and existing underscores with single underscore
84+
.replace(/[\s-]+/g, '_')
85+
// Remove multiple consecutive underscores
86+
.replace(/_+/g, '_')
87+
// Remove leading/trailing underscores and convert to uppercase
88+
.replace(/^_+|_+$/g, '')
89+
.toUpperCase();
90+
}

0 commit comments

Comments
 (0)