diff --git a/src/index.ts b/src/index.ts index f1983fd..07a71b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -113,7 +113,23 @@ export function camelCase< T extends string | readonly string[], UserCaseOptions extends CaseOptions = CaseOptions, >(str?: T, opts?: UserCaseOptions) { - return lowerFirst(pascalCase(str || "", opts)) as CamelCase< + if (!str) { + return ""; + } + const rawParts = Array.isArray(str) ? str : splitByCase(str as string); + const parts = rawParts.filter(Boolean); + if (parts.length === 0) { + return ""; + } + const [first = "", ...rest] = parts; + const firstFormatted = + opts?.normalize || first === first.toUpperCase() + ? first.toLowerCase() + : lowerFirst(first); + const restFormatted = rest.map((p) => + upperFirst(opts?.normalize ? p.toLowerCase() : p), + ); + return (firstFormatted + restFormatted.join("")) as CamelCase< T, UserCaseOptions["normalize"] >; diff --git a/src/types.ts b/src/types.ts index 9e7f1dd..69f4734 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,14 @@ type CapitalizedWords< Normalize > : Accumulator; +type CamelCaseWords< + T extends readonly string[], + Normalize extends boolean | undefined = false, +> = T extends readonly [infer F extends string, ...infer R extends string[]] + ? F extends "" + ? CamelCaseWords + : `${Normalize extends true ? Lowercase : F extends Uppercase ? Lowercase : Uncapitalize}${CapitalizedWords}` + : ""; type JoinLowercaseWords< T extends readonly string[], Joiner extends string, @@ -133,7 +141,13 @@ export type CamelCase< ? string : string[] extends T ? string - : Uncapitalize>; + : T extends string + ? SplitByCase extends readonly string[] + ? CamelCaseWords, Normalize> + : never + : T extends readonly string[] + ? CamelCaseWords + : never; export type KebabCase< T extends string | readonly string[], diff --git a/test/scule.test.ts b/test/scule.test.ts index bd5ec3b..cd7107f 100644 --- a/test/scule.test.ts +++ b/test/scule.test.ts @@ -57,9 +57,27 @@ describe("pascalCase", () => { describe("camelCase", () => { test.each([ + ["", ""], + ["-", ""], + [[], ""], ["FooBarBaz", "fooBarBaz"], - ["FOO_BAR", "fooBar"], + ["APIBaseURL", "apiBaseURL"], + ["XMLParser", "xmlParser"], + ["HTTPSResponse", "httpsResponse"], + ["-foo", "foo"], + ["--foo-bar--", "fooBar"], + [["fooBar", "Baz"], "fooBarBaz"], + [["API", "Base", "URL"], "apiBaseURL"], ])("%s => %s", (input, expected) => { + expect(camelCase(input)).toMatchObject(expected); + }); + + test.each([ + ["FooBarBaz", "fooBarBaz"], + ["FOO_BAR", "fooBar"], + ["API_Base_URL", "apiBaseUrl"], + ["MY_API_KEY", "myApiKey"], + ])("%s (normalized) => %s", (input, expected) => { expect(camelCase(input, { normalize: true })).toMatchObject(expected); }); }); diff --git a/test/types.test-d.ts b/test/types.test-d.ts index bd722b1..b49676b 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -72,10 +72,14 @@ describe("CamelCase", () => { assertType>("fooBaRb"); assertType>("fooBarBazQux"); assertType>("fooBar"); + assertType>("apiBaseURL"); + assertType>("apiBaseUrl"); }); test("array", () => { assertType>("fooBar"); + assertType>("fooBarBaz"); + assertType>("apiBaseURL"); }); });