Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(titleCase): insert extra space when convert valid string #97

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function splitByCase<
// Splitter
const isSplitter = (splitters as unknown as string).includes(char);
if (isSplitter === true) {
parts.push(buff);
parts.push(buff.trim());
buff = "";
previousUpper = undefined;
continue;
Expand All @@ -54,15 +54,15 @@ export function splitByCase<
if (previousSplitter === false) {
// Case rising edge
if (previousUpper === false && isUpper === true) {
parts.push(buff);
parts.push(buff.trim());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we could fix algorithm that does not add trailing space in first place?

buff = char;
previousUpper = isUpper;
continue;
}
// Case falling edge
if (previousUpper === true && isUpper === false && buff.length > 1) {
const lastChar = buff.at(-1);
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
parts.push(buff.slice(0, Math.max(0, buff.length - 1)).trim());
buff = lastChar + char;
previousUpper = isUpper;
continue;
Expand All @@ -75,7 +75,7 @@ export function splitByCase<
previousSplitter = isSplitter;
}

parts.push(buff);
parts.push(buff.trim());

return parts as SplitByCase<T, Separator[number]>;
}
Expand Down
69 changes: 57 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,27 @@ type SameLetterCase<X extends string, Y extends string> =
: IsLower<X> extends IsLower<Y>
? true
: false;

type CapitalizedWords<
T extends readonly string[],
Accumulator extends string = "",
Joiner extends string,
Normalize extends boolean | undefined = false,
> = T extends readonly [infer F extends string, ...infer R extends string[]]
? CapitalizedWords<
R,
`${Accumulator}${Capitalize<Normalize extends true ? Lowercase<F> : F>}`,
Normalize
>
: Accumulator;
Accumulator extends string = "",
> = T extends readonly []
? Accumulator
: T extends readonly [infer F extends string, ...infer R extends string[]]
? CapitalizedWords<
R,
Joiner,
Normalize,
`${Accumulator}${Capitalize<Normalize extends true ? Lowercase<F> : F>}${[
LastOfArray<R>,
] extends [never]
? ""
: Joiner}`
>
: Accumulator;

type JoinLowercaseWords<
T extends readonly string[],
Joiner extends string,
Expand All @@ -39,6 +49,41 @@ type RemoveLastOfArray<T extends any[]> = T extends [...infer F, any]
? F
: never;

type Whitespace =
| "\u{9}" // '\t'
| "\u{A}" // '\n'
| "\u{B}" // '\v'
| "\u{C}" // '\f'
| "\u{D}" // '\r'
| "\u{20}" // ' '
| "\u{85}"
| "\u{A0}"
| "\u{1680}"
| "\u{2000}"
| "\u{2001}"
| "\u{2002}"
| "\u{2003}"
| "\u{2004}"
| "\u{2005}"
| "\u{2006}"
| "\u{2007}"
| "\u{2008}"
| "\u{2009}"
| "\u{200A}"
| "\u{2028}"
| "\u{2029}"
| "\u{202F}"
| "\u{205F}"
| "\u{3000}"
| "\u{FEFF}";
type TrimLeft<T extends string> = T extends `${Whitespace}${infer R}`
? TrimLeft<R>
: T;
type TrimRight<T extends string> = T extends `${infer R}${Whitespace}`
? TrimRight<R>
: T;
type Trim<T extends string> = TrimLeft<TrimRight<T>>;

export type CaseOptions = {
normalize?: boolean;
};
Expand All @@ -51,15 +96,15 @@ export type SplitByCase<
? string[]
: T extends `${infer F}${infer R}`
? [LastOfArray<Accumulator>] extends [never]
? SplitByCase<R, Separator, [F]>
? SplitByCase<R, Separator, F extends Separator | Whitespace ? [] : [F]>
: LastOfArray<Accumulator> extends string
? R extends ""
? SplitByCase<
R,
Separator,
[
...RemoveLastOfArray<Accumulator>,
`${LastOfArray<Accumulator>}${F}`,
Trim<`${LastOfArray<Accumulator>}${F}`>,
]
>
: SameLetterCase<F, FirstOfString<R>> extends true
Expand All @@ -78,7 +123,7 @@ export type SplitByCase<
Separator,
[
...RemoveLastOfArray<Accumulator>,
`${LastOfArray<Accumulator>}${F}`,
Trim<`${LastOfArray<Accumulator>}${F}`>,
]
>
: IsLower<F> extends true
Expand All @@ -87,7 +132,7 @@ export type SplitByCase<
Separator,
[
...RemoveLastOfArray<Accumulator>,
`${LastOfArray<Accumulator>}${F}`,
Trim<`${LastOfArray<Accumulator>}${F}`>,
FirstOfString<R>,
]
>
Expand Down
11 changes: 11 additions & 0 deletions test/scule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ describe("splitByCase", () => {
["foo123-bar", ["foo123", "bar"]],
["FOOBar", ["FOO", "Bar"]],
["ALink", ["A", "Link"]],
["-FooBar", ["", "Foo", "Bar"]],
[" FooBar", ["", "Foo", "Bar"]],
// with custom splitters
[
"foo\\Bar.fuzz-FIZz",
Expand All @@ -50,6 +52,7 @@ describe("pascalCase", () => {
["foo_bar-baz/qux", "FooBarBazQux"],
["FOO_BAR", "FooBar"],
["foo--bar-Baz", "FooBarBaz"],
["FooBarBazQux", "FooBarBazQux"],
])("%s => %s", (input, expected) => {
expect(pascalCase(input, { normalize: true })).toMatchObject(expected);
});
Expand All @@ -59,6 +62,7 @@ describe("camelCase", () => {
test.each([
["FooBarBaz", "fooBarBaz"],
["FOO_BAR", "fooBar"],
["fooBarBaz", "fooBarBaz"],
])("%s => %s", (input, expected) => {
expect(camelCase(input, { normalize: true })).toMatchObject(expected);
});
Expand All @@ -74,6 +78,7 @@ describe("kebabCase", () => {
["FooBAR", "foo-bar"],
["ALink", "a-link"],
["FOO_BAR", "foo-bar"],
["foo-b-ar", "foo-b-ar"],
])("%s => %s", (input, expected) => {
expect(kebabCase(input)).toMatchObject(expected);
});
Expand All @@ -83,6 +88,7 @@ describe("snakeCase", () => {
test.each([
["FooBarBaz", "foo_bar_baz"],
["FOO_BAR", "foo_bar"],
["foo_bar_baz", "foo_bar_baz"],
])("%s => %s", (input, expected) => {
expect(snakeCase(input)).toMatchObject(expected);
});
Expand All @@ -93,6 +99,7 @@ describe("upperFirst", () => {
["", ""],
["foo", "Foo"],
["Foo", "Foo"],
["FooBarBaz", "FooBarBaz"],
])("%s => %s", (input, expected) => {
expect(upperFirst(input)).toMatchObject(expected);
});
Expand All @@ -103,6 +110,7 @@ describe("lowerFirst", () => {
["", ""],
["foo", "foo"],
["Foo", "foo"],
["fooBarBaz", "fooBarBaz"],
])("%s => %s", (input, expected) => {
expect(lowerFirst(input)).toMatchObject(expected);
});
Expand All @@ -120,6 +128,7 @@ describe("trainCase", () => {
["foo--bar-Baz", "Foo-Bar-Baz"],
["WWW-authenticate", "WWW-Authenticate"],
["WWWAuthenticate", "WWW-Authenticate"],
["Foo-B-Ar", "Foo-B-Ar"],
])("%s => %s", (input, expected) => {
expect(trainCase(input)).toMatchObject(expected);
});
Expand All @@ -140,6 +149,7 @@ describe("titleCase", () => {
["foo", "Foo"],
["foo-bar", "Foo Bar"],
["this-IS-aTitle", "This is a Title"],
["Foo Bar", "Foo Bar"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add at least to types test as well? (for types we can do simply similar fix with trim should be fine)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some changes to types and added types test cases for TrainCase and TitleCase(TrainCase with space joiner).

There are still two cases didn't pass. That's because titleCase and trainCase use filter(Boolean) to filter empty string. I don't know how to fix the type properly so I add a @ts-expect-error comment above it.

Could you do some help?

])("%s => %s", (input, expected) => {
expect(titleCase(input)).toMatchObject(expected);
});
Expand All @@ -154,6 +164,7 @@ describe("flatCase", () => {
["foo_bar-baz/qux", "foobarbazqux"],
["FOO_BAR", "foobar"],
["foo--bar-Baz", "foobarbaz"],
["foobarbaz", "foobarbaz"],
])("%s => %s", (input, expected) => {
expect(flatCase(input)).toMatchObject(expected);
});
Expand Down
56 changes: 56 additions & 0 deletions test/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
PascalCase,
CamelCase,
JoinByCase,
TrainCase,
} from "../src/types";

describe("SplitByCase", () => {
Expand All @@ -22,6 +23,9 @@ describe("SplitByCase", () => {
assertType<SplitByCase<"FOOBar">>(["FOO", "Bar"]);
assertType<SplitByCase<"ALink">>(["A", "Link"]);
assertType<SplitByCase<"FOO_BAR">>(["FOO", "BAR"]);
assertType<SplitByCase<"Foo Bar Baz">>(["Foo", "Bar", "Baz"]);
assertType<SplitByCase<"-Bar Baz">>(["Bar", "Baz"]);
assertType<SplitByCase<" Bar Baz">>(["Bar", "Baz"]);
});

test("custom splitters", () => {
Expand Down Expand Up @@ -50,6 +54,7 @@ describe("PascalCase", () => {
assertType<PascalCase<"foo_bar-baz/qux", true>>("FooBarBazQux");
assertType<PascalCase<"foo--bar-Baz", true>>("FooBarBaz");
assertType<PascalCase<"FOO_BAR", true>>("FooBar");
assertType<PascalCase<"FooBarBaz", true>>("FooBarBaz");
});

test("array", () => {
Expand All @@ -72,6 +77,7 @@ describe("CamelCase", () => {
assertType<CamelCase<"FooBARb", true>>("fooBaRb");
assertType<CamelCase<"foo_bar-baz/qux", true>>("fooBarBazQux");
assertType<CamelCase<"FOO_BAR", true>>("fooBar");
assertType<CamelCase<"fooBarBaz", true>>("fooBarBaz");
});

test("array", () => {
Expand All @@ -90,9 +96,59 @@ describe("JoinByCase", () => {
assertType<JoinByCase<"foo", "-">>("foo");
assertType<JoinByCase<"FooBARb", "-">>("foo-ba-rb");
assertType<JoinByCase<"foo_bar-baz/qux", "-">>("foo-bar-baz-qux");
assertType<JoinByCase<"foo-bar-baz", "-">>("foo-bar-baz");
});

test("array", () => {
assertType<JoinByCase<["Foo", "Bar"], "-">>("foo-bar");
});
});

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

test("string", () => {
assertType<TrainCase<"">>("");
assertType<TrainCase<"f">>("F");
assertType<TrainCase<"foo">>("Foo");
assertType<TrainCase<"foo-bAr">>("Foo-B-Ar");
assertType<TrainCase<"AcceptCH">>("Accept-CH");
assertType<TrainCase<"foo_bar-baz/qux">>("Foo-Bar-Baz-Qux");
assertType<TrainCase<"FOO_BAR">>("FOO-BAR");
// @ts-expect-error - splitByCase result may contain empty string
assertType<TrainCase<"foo--bar-Baz">>("Foo-Bar-Baz");
assertType<TrainCase<"WWW-authenticate">>("WWW-Authenticate");
assertType<TrainCase<"WWWAuthenticate">>("WWW-Authenticate");
assertType<TrainCase<"Foo-B-Ar">>("Foo-B-Ar");
});

test("array", () => {
assertType<TrainCase<[]>>("");
assertType<TrainCase<["foo", "bar"]>>("Foo-Bar");
});
});

describe("TitleCase", () => {
test("string", () => {
assertType<TrainCase<"">>("");
assertType<TrainCase<"f", false, " ">>("F");
assertType<TrainCase<"foo", false, " ">>("Foo");
assertType<TrainCase<"foo-bAr", false, " ">>("Foo B Ar");
assertType<TrainCase<"AcceptCH", false, " ">>("Accept CH");
assertType<TrainCase<"foo_bar-baz/qux", false, " ">>("Foo Bar Baz Qux");
assertType<TrainCase<"FOO_BAR", false, " ">>("FOO BAR");
// @ts-expect-error - splitByCase result may contain empty string
assertType<TrainCase<"foo--bar-Baz", false, " ">>("Foo Bar Baz");
assertType<TrainCase<"WWW-authenticate", false, " ">>("WWW Authenticate");
assertType<TrainCase<"WWWAuthenticate", false, " ">>("WWW Authenticate");
assertType<TrainCase<"Foo-B-Ar", false, " ">>("Foo B Ar");
});

test("array", () => {
assertType<TrainCase<[]>>("");
assertType<TrainCase<["foo", "bar"], false, " ">>("Foo Bar");
});
});