Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/inflekt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"makage": "0.1.10"
},
"dependencies": {
"inflection": "^3.0.0"
"inflection": "^3.0.0",
"komoji": "workspace:*"
}
}
71 changes: 23 additions & 48 deletions packages/inflekt/src/case.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
/**
* Case transformation utilities
*
* Pure case transforms are delegated to komoji (the origin of truth).
* This module re-exports them for backward compatibility and adds
* inflekt-specific helpers (fixCapitalisedPlural, underscore, toScreamingSnake).
*/

/**
* Convert PascalCase to camelCase (lowercase first character)
* @example "UserProfile" -> "userProfile"
*/
export function lcFirst(str: string): string {
return str.charAt(0).toLowerCase() + str.slice(1);
}
import {
lcFirst,
ucFirst,
toCamelCase as _toCamelCase,
toPascalCase,
toSnakeCase,
toConstantCase,
} from 'komoji';

/**
* Convert camelCase to PascalCase (uppercase first character)
* @example "userProfile" -> "UserProfile"
*/
export function ucFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
// Re-export komoji functions directly
export { lcFirst, ucFirst, toPascalCase, toSnakeCase, toConstantCase };

// Re-export toCamelCase — komoji's version accepts a second parameter
// (stripLeadingNonAlphabetChars) so we wrap to keep the simpler inflekt signature
export function toCamelCase(str: string): string {
return _toCamelCase(str);
}

/**
Expand All @@ -29,51 +35,20 @@ export function fixCapitalisedPlural(str: string): string {

/**
* Convert PascalCase or camelCase to snake_case
* @deprecated Use toSnakeCase from komoji instead
* @example underscore('UserProfile') -> 'user_profile'
* @example underscore('userProfile') -> 'user_profile'
*/
export function underscore(str: string): string {
return str
.replace(/([A-Z])/g, '_$1')
.replace(/^_/, '')
.toLowerCase();
}

/**
* Convert a hyphenated, underscored, or already-camelCased string to camelCase.
* Handles both `-` and `_` delimiters.
* @example toCamelCase('user-profile') -> 'userProfile'
* @example toCamelCase('user_profile') -> 'userProfile'
* @example toCamelCase('UserProfile') -> 'userProfile'
*/
export function toCamelCase(str: string): string {
return str
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
.replace(/^(.)/, (_, char) => char.toLowerCase());
}

/**
* Convert a hyphenated, underscored, or already-camelCased string to PascalCase.
* Handles both `-` and `_` delimiters.
* @example toPascalCase('user-profile') -> 'UserProfile'
* @example toPascalCase('user_profile') -> 'UserProfile'
* @example toPascalCase('userProfile') -> 'UserProfile'
*/
export function toPascalCase(str: string): string {
return str
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
.replace(/^(.)/, (_, char) => char.toUpperCase());
return toSnakeCase(str);
}

/**
* Convert a camelCase or PascalCase string to SCREAMING_SNAKE_CASE.
* @deprecated Use toConstantCase from komoji instead
* @example toScreamingSnake('userProfile') -> 'USER_PROFILE'
* @example toScreamingSnake('UserProfile') -> 'USER_PROFILE'
*/
export function toScreamingSnake(str: string): string {
return str
.replace(/([A-Z])/g, '_$1')
.replace(/[-\s]/g, '_')
.toUpperCase()
.replace(/^_/, '');
return toConstantCase(str);
}
53 changes: 53 additions & 0 deletions packages/komoji/__tests__/casing.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
lcFirst,
ucFirst,
isValidIdentifier,
isValidIdentifierCamelized,
toCamelCase,
Expand Down Expand Up @@ -41,6 +43,40 @@ it('should validate valid JavaScript-like identifiers allowing internal hyphens'
expect(isValidIdentifierCamelized('invalid-identifier-')).toBe(true);
});

describe('lcFirst', () => {
test('lowercases the first character', () => {
expect(lcFirst('UserProfile')).toBe('userProfile');
expect(lcFirst('User')).toBe('user');
expect(lcFirst('ABC')).toBe('aBC');
});

test('handles already lowercase strings', () => {
expect(lcFirst('user')).toBe('user');
});

test('handles single character', () => {
expect(lcFirst('A')).toBe('a');
expect(lcFirst('a')).toBe('a');
});
});

describe('ucFirst', () => {
test('uppercases the first character', () => {
expect(ucFirst('userProfile')).toBe('UserProfile');
expect(ucFirst('user')).toBe('User');
expect(ucFirst('abc')).toBe('Abc');
});

test('handles already uppercase strings', () => {
expect(ucFirst('User')).toBe('User');
});

test('handles single character', () => {
expect(ucFirst('a')).toBe('A');
expect(ucFirst('A')).toBe('A');
});
});

describe('toPascalCase', () => {
test('converts normal string', () => {
expect(toPascalCase('hello_world')).toBe('HelloWorld');
Expand All @@ -56,6 +92,18 @@ describe('toPascalCase', () => {
expect(toPascalCase('hello___world--great')).toBe('HelloWorldGreat');
});

test('handles consecutive mixed separators', () => {
expect(toPascalCase('my__double_under')).toBe('MyDoubleUnder');
expect(toPascalCase('my-_mixed-_sep')).toBe('MyMixedSep');
expect(toPascalCase('my--double-dash')).toBe('MyDoubleDash');
});

test('handles leading separators', () => {
expect(toPascalCase('_private')).toBe('Private');
expect(toPascalCase('__double')).toBe('Double');
expect(toPascalCase('-leading')).toBe('Leading');
});

test('handles single word', () => {
expect(toPascalCase('word')).toBe('Word');
});
Expand All @@ -67,6 +115,11 @@ describe('toPascalCase', () => {
test('handles string with numbers', () => {
expect(toPascalCase('version1_2_3')).toBe('Version123');
});

test('handles spaces', () => {
expect(toPascalCase('my table name')).toBe('MyTableName');
expect(toPascalCase('My Table Name')).toBe('MyTableName');
});
});

describe('toCamelCase', () => {
Expand Down
24 changes: 20 additions & 4 deletions packages/komoji/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
/**
* Lowercase the first character of a string
* @example "UserProfile" -> "userProfile"
*/
export function lcFirst(str: string): string {
return str.charAt(0).toLowerCase() + str.slice(1);
}

/**
* Uppercase the first character of a string
* @example "userProfile" -> "UserProfile"
*/
export function ucFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

export function toPascalCase(str: string) {
return str
.replace(/(^|_|\s|-)(\w)/g, (_: any, __: any, letter: string) =>
letter.toUpperCase()
)
.replace(/[_\s-]/g, '');
// Convert what follows one-or-more separators into upper case (handles consecutive separators)
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
// Ensure the first character is always uppercase
.replace(/^./, (c) => c.toUpperCase());
}

export function toCamelCase(
Expand Down
Loading
Loading