diff --git a/README.md b/README.md index 6c7991a..bc87c0c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ import { pascalCase } from "scule"; ## Utils -### `pascalCase(str)` +### `pascalCase(str, opts?: { normalize })` Splits string and joins by PascalCase convention: @@ -38,9 +38,9 @@ pascalCase("foo-bar_baz"); // FooBarBaz ``` -**Notice:** If an uppercase letter is followed by other uppercase letters (like `FooBAR`), they are preserved. +**Notice:** If an uppercase letter is followed by other uppercase letters (like `FooBAR`), they are preserved. You can use `{ normalize: true }` for strictly following pascalCase convention. -### `camelCase` +### `camelCase(str, opts?: { normalize })` Splits string and joins by camelCase convention: diff --git a/src/index.ts b/src/index.ts index cd5d9f1..afd7485 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import type { PascalCase, SnakeCase, SplitByCase, + CaseOptions, } from "./types"; const NUMBER_CHAR_RE = /\d/; @@ -86,23 +87,34 @@ export function lowerFirst(str: S): Uncapitalize { } export function pascalCase(): ""; -export function pascalCase( - str: T, -): PascalCase; -export function pascalCase(str?: T) { +export function pascalCase< + T extends string | readonly string[], + UserCaseOptions extends CaseOptions = CaseOptions, +>(str: T, opts?: CaseOptions): PascalCase; +export function pascalCase< + T extends string | readonly string[], + UserCaseOptions extends CaseOptions = CaseOptions, +>(str?: T, opts?: UserCaseOptions) { return str ? ((Array.isArray(str) ? str : splitByCase(str as string)) - .map((p) => upperFirst(p.toLowerCase())) - .join("") as PascalCase) + .map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)) + .join("") as PascalCase) : ""; } export function camelCase(): ""; -export function camelCase( - str: T, -): CamelCase; -export function camelCase(str?: T) { - return lowerFirst(pascalCase(str || "")) as CamelCase; +export function camelCase< + T extends string | readonly string[], + UserCaseOptions extends CaseOptions = CaseOptions, +>(str: T, opts?: UserCaseOptions): CamelCase; +export function camelCase< + T extends string | readonly string[], + UserCaseOptions extends CaseOptions = CaseOptions, +>(str?: T, opts?: UserCaseOptions) { + return lowerFirst(pascalCase(str || "", opts)) as CamelCase< + T, + UserCaseOptions["normalize"] + >; } export function kebabCase(): ""; diff --git a/src/types.ts b/src/types.ts index 573e874..d1e72e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,8 +18,13 @@ type SameLetterCase< type CapitalizedWords< T extends readonly string[], Accumulator extends string = "", + Normalize extends boolean | undefined = false, > = T extends readonly [infer F extends string, ...infer R extends string[]] - ? CapitalizedWords>}`> + ? CapitalizedWords< + R, + `${Accumulator}${Capitalize : F>}`, + Normalize + > : Accumulator; type JoinLowercaseWords< T extends readonly string[], @@ -36,6 +41,10 @@ type RemoveLastOfArray = T extends [...infer F, any] ? F : never; +export type CaseOptions = { + normalize?: boolean; +}; + export type SplitByCase< T, Separator extends string = Splitter, @@ -104,23 +113,29 @@ export type JoinByCase = string extends T ? JoinLowercaseWords : never; -export type PascalCase = string extends T +export type PascalCase< + T, + Normalize extends boolean | undefined = false, +> = string extends T ? string : string[] extends T ? string : T extends string ? SplitByCase extends readonly string[] - ? CapitalizedWords> + ? CapitalizedWords, "", Normalize> : never : T extends readonly string[] - ? CapitalizedWords + ? CapitalizedWords : never; -export type CamelCase = string extends T +export type CamelCase< + T, + Normalize extends boolean | undefined = false, +> = string extends T ? string : string[] extends T ? string - : Uncapitalize>; + : Uncapitalize>; export type KebabCase< T extends string | readonly string[], diff --git a/test/scule.test.ts b/test/scule.test.ts index cb17472..08fa309 100644 --- a/test/scule.test.ts +++ b/test/scule.test.ts @@ -47,7 +47,7 @@ describe("pascalCase", () => { ["FOO_BAR", "FooBar"], ["foo--bar-Baz", "FooBarBaz"], ])("%s => %s", (input, expected) => { - expect(pascalCase(input)).toMatchObject(expected); + expect(pascalCase(input, { normalize: true })).toMatchObject(expected); }); }); @@ -56,7 +56,7 @@ describe("camelCase", () => { ["FooBarBaz", "fooBarBaz"], ["FOO_BAR", "fooBar"], ])("%s => %s", (input, expected) => { - expect(camelCase(input)).toMatchObject(expected); + expect(camelCase(input, { normalize: true })).toMatchObject(expected); }); }); diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 17267e7..4a9a4a8 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -37,23 +37,23 @@ describe("SplitByCase", () => { describe("PascalCase", () => { test("types", () => { - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); test("string", () => { - assertType>(""); - assertType>("Foo"); - assertType>("FooBAr"); - assertType>("FooBaRb"); - assertType>("FooBarBazQux"); - assertType>("FooBarBaz"); - assertType>("FooBar"); + assertType>(""); + assertType>("Foo"); + assertType>("FooBAr"); + assertType>("FooBaRb"); + assertType>("FooBarBazQux"); + assertType>("FooBarBaz"); + assertType>("FooBar"); }); test("array", () => { - assertType>("FooBar"); - assertType>( + assertType>("FooBar"); + assertType>( "FooBarFuzzFiZz", ); }); @@ -61,20 +61,20 @@ describe("PascalCase", () => { describe("CamelCase", () => { test("types", () => { - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); test("string", () => { - assertType>(""); - assertType>("foo"); - assertType>("fooBaRb"); - assertType>("fooBarBazQux"); - assertType>("fooBar"); + assertType>(""); + assertType>("foo"); + assertType>("fooBaRb"); + assertType>("fooBarBazQux"); + assertType>("fooBar"); }); test("array", () => { - assertType>("fooBar"); + assertType>("fooBar"); }); });