Skip to content

Commit

Permalink
fix(pascalCase, camelCase): move normalize behind a flag (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Nov 30, 2023
1 parent 4c9da31 commit e32421a
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 41 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { pascalCase } from "scule";

## Utils

### `pascalCase(str)`
### `pascalCase(str, opts?: { normalize })`

Splits string and joins by PascalCase convention:

Expand All @@ -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:

Expand Down
34 changes: 23 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
PascalCase,
SnakeCase,
SplitByCase,
CaseOptions,
} from "./types";

const NUMBER_CHAR_RE = /\d/;
Expand Down Expand Up @@ -86,23 +87,34 @@ export function lowerFirst<S extends string>(str: S): Uncapitalize<S> {
}

export function pascalCase(): "";
export function pascalCase<T extends string | readonly string[]>(
str: T,
): PascalCase<T>;
export function pascalCase<T extends string | readonly string[]>(str?: T) {
export function pascalCase<
T extends string | readonly string[],
UserCaseOptions extends CaseOptions = CaseOptions,
>(str: T, opts?: CaseOptions): PascalCase<T, UserCaseOptions["normalize"]>;
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<T>)
.map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p))
.join("") as PascalCase<T, UserCaseOptions["normalize"]>)
: "";
}

export function camelCase(): "";
export function camelCase<T extends string | readonly string[]>(
str: T,
): CamelCase<T>;
export function camelCase<T extends string | readonly string[]>(str?: T) {
return lowerFirst(pascalCase(str || "")) as CamelCase<T>;
export function camelCase<
T extends string | readonly string[],
UserCaseOptions extends CaseOptions = CaseOptions,
>(str: T, opts?: UserCaseOptions): CamelCase<T, UserCaseOptions["normalize"]>;
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(): "";
Expand Down
27 changes: 21 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<R, `${Accumulator}${Capitalize<Lowercase<F>>}`>
? CapitalizedWords<
R,
`${Accumulator}${Capitalize<Normalize extends true ? Lowercase<F> : F>}`,
Normalize
>
: Accumulator;
type JoinLowercaseWords<
T extends readonly string[],
Expand All @@ -36,6 +41,10 @@ type RemoveLastOfArray<T extends any[]> = T extends [...infer F, any]
? F
: never;

export type CaseOptions = {
normalize?: boolean;
};

export type SplitByCase<
T,
Separator extends string = Splitter,
Expand Down Expand Up @@ -104,23 +113,29 @@ export type JoinByCase<T, Joiner extends string> = string extends T
? JoinLowercaseWords<T, Joiner>
: never;

export type PascalCase<T> = string extends T
export type PascalCase<
T,
Normalize extends boolean | undefined = false,
> = string extends T
? string
: string[] extends T
? string
: T extends string
? SplitByCase<T> extends readonly string[]
? CapitalizedWords<SplitByCase<T>>
? CapitalizedWords<SplitByCase<T>, "", Normalize>
: never
: T extends readonly string[]
? CapitalizedWords<T>
? CapitalizedWords<T, "", Normalize>
: never;

export type CamelCase<T> = string extends T
export type CamelCase<
T,
Normalize extends boolean | undefined = false,
> = string extends T
? string
: string[] extends T
? string
: Uncapitalize<PascalCase<T>>;
: Uncapitalize<PascalCase<T, Normalize>>;

export type KebabCase<
T extends string | readonly string[],
Expand Down
4 changes: 2 additions & 2 deletions test/scule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand All @@ -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);
});
});

Expand Down
38 changes: 19 additions & 19 deletions test/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,44 +37,44 @@ describe("SplitByCase", () => {

describe("PascalCase", () => {
test("types", () => {
expectTypeOf<PascalCase<string>>().toEqualTypeOf<string>();
expectTypeOf<PascalCase<string[]>>().toEqualTypeOf<string>();
expectTypeOf<PascalCase<string, true>>().toEqualTypeOf<string>();
expectTypeOf<PascalCase<string[], true>>().toEqualTypeOf<string>();
});

test("string", () => {
assertType<PascalCase<"">>("");
assertType<PascalCase<"foo">>("Foo");
assertType<PascalCase<"foo-bAr">>("FooBAr");
assertType<PascalCase<"FooBARb">>("FooBaRb");
assertType<PascalCase<"foo_bar-baz/qux">>("FooBarBazQux");
assertType<PascalCase<"foo--bar-Baz">>("FooBarBaz");
assertType<PascalCase<"FOO_BAR">>("FooBar");
assertType<PascalCase<"", true>>("");
assertType<PascalCase<"foo", true>>("Foo");
assertType<PascalCase<"foo-bAr", true>>("FooBAr");
assertType<PascalCase<"FooBARb", true>>("FooBaRb");
assertType<PascalCase<"foo_bar-baz/qux", true>>("FooBarBazQux");
assertType<PascalCase<"foo--bar-Baz", true>>("FooBarBaz");
assertType<PascalCase<"FOO_BAR", true>>("FooBar");
});

test("array", () => {
assertType<PascalCase<["foo", "Bar"]>>("FooBar");
assertType<PascalCase<["foo", "Bar", "fuzz", "FI", "Zz"]>>(
assertType<PascalCase<["foo", "Bar"], true>>("FooBar");
assertType<PascalCase<["foo", "Bar", "fuzz", "FI", "Zz"], true>>(
"FooBarFuzzFiZz",
);
});
});

describe("CamelCase", () => {
test("types", () => {
expectTypeOf<CamelCase<string>>().toEqualTypeOf<string>();
expectTypeOf<CamelCase<string[]>>().toEqualTypeOf<string>();
expectTypeOf<CamelCase<string, true>>().toEqualTypeOf<string>();
expectTypeOf<CamelCase<string[], true>>().toEqualTypeOf<string>();
});

test("string", () => {
assertType<CamelCase<"">>("");
assertType<CamelCase<"foo">>("foo");
assertType<CamelCase<"FooBARb">>("fooBaRb");
assertType<CamelCase<"foo_bar-baz/qux">>("fooBarBazQux");
assertType<CamelCase<"FOO_BAR">>("fooBar");
assertType<CamelCase<"", true>>("");
assertType<CamelCase<"foo", true>>("foo");
assertType<CamelCase<"FooBARb", true>>("fooBaRb");
assertType<CamelCase<"foo_bar-baz/qux", true>>("fooBarBazQux");
assertType<CamelCase<"FOO_BAR", true>>("fooBar");
});

test("array", () => {
assertType<CamelCase<["Foo", "Bar"]>>("fooBar");
assertType<CamelCase<["Foo", "Bar"], true>>("fooBar");
});
});

Expand Down

0 comments on commit e32421a

Please sign in to comment.