diff --git a/src/index.ts b/src/index.ts index f1983fd..7169cf1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; @@ -54,7 +54,7 @@ export function splitByCase< if (previousSplitter === false) { // Case rising edge if (previousUpper === false && isUpper === true) { - parts.push(buff); + parts.push(buff.trim()); buff = char; previousUpper = isUpper; continue; @@ -62,7 +62,7 @@ export function splitByCase< // 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; @@ -75,7 +75,7 @@ export function splitByCase< previousSplitter = isSplitter; } - parts.push(buff); + parts.push(buff.trim()); return parts as SplitByCase; } diff --git a/src/types.ts b/src/types.ts index 9e7f1dd..b92f967 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,17 +13,27 @@ type SameLetterCase = : IsLower extends IsLower ? 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 : 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 : F>}${[ + LastOfArray, + ] extends [never] + ? "" + : Joiner}` + > + : Accumulator; + type JoinLowercaseWords< T extends readonly string[], Joiner extends string, @@ -39,6 +49,41 @@ type RemoveLastOfArray = 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 `${Whitespace}${infer R}` + ? TrimLeft + : T; +type TrimRight = T extends `${infer R}${Whitespace}` + ? TrimRight + : T; +type Trim = TrimLeft>; + export type CaseOptions = { normalize?: boolean; }; @@ -51,7 +96,7 @@ export type SplitByCase< ? string[] : T extends `${infer F}${infer R}` ? [LastOfArray] extends [never] - ? SplitByCase + ? SplitByCase : LastOfArray extends string ? R extends "" ? SplitByCase< @@ -59,7 +104,7 @@ export type SplitByCase< Separator, [ ...RemoveLastOfArray, - `${LastOfArray}${F}`, + Trim<`${LastOfArray}${F}`>, ] > : SameLetterCase> extends true @@ -78,7 +123,7 @@ export type SplitByCase< Separator, [ ...RemoveLastOfArray, - `${LastOfArray}${F}`, + Trim<`${LastOfArray}${F}`>, ] > : IsLower extends true @@ -87,7 +132,7 @@ export type SplitByCase< Separator, [ ...RemoveLastOfArray, - `${LastOfArray}${F}`, + Trim<`${LastOfArray}${F}`>, FirstOfString, ] > diff --git a/test/scule.test.ts b/test/scule.test.ts index bd5ec3b..6a36822 100644 --- a/test/scule.test.ts +++ b/test/scule.test.ts @@ -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", @@ -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); }); @@ -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); }); @@ -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); }); @@ -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); }); @@ -93,6 +99,7 @@ describe("upperFirst", () => { ["", ""], ["foo", "Foo"], ["Foo", "Foo"], + ["FooBarBaz", "FooBarBaz"], ])("%s => %s", (input, expected) => { expect(upperFirst(input)).toMatchObject(expected); }); @@ -103,6 +110,7 @@ describe("lowerFirst", () => { ["", ""], ["foo", "foo"], ["Foo", "foo"], + ["fooBarBaz", "fooBarBaz"], ])("%s => %s", (input, expected) => { expect(lowerFirst(input)).toMatchObject(expected); }); @@ -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); }); @@ -140,6 +149,7 @@ describe("titleCase", () => { ["foo", "Foo"], ["foo-bar", "Foo Bar"], ["this-IS-aTitle", "This is a Title"], + ["Foo Bar", "Foo Bar"], ])("%s => %s", (input, expected) => { expect(titleCase(input)).toMatchObject(expected); }); @@ -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); }); diff --git a/test/types.test-d.ts b/test/types.test-d.ts index bd722b1..fcf04ec 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -4,6 +4,7 @@ import type { PascalCase, CamelCase, JoinByCase, + TrainCase, } from "../src/types"; describe("SplitByCase", () => { @@ -22,6 +23,9 @@ describe("SplitByCase", () => { assertType>(["FOO", "Bar"]); assertType>(["A", "Link"]); assertType>(["FOO", "BAR"]); + assertType>(["Foo", "Bar", "Baz"]); + assertType>(["Bar", "Baz"]); + assertType>(["Bar", "Baz"]); }); test("custom splitters", () => { @@ -50,6 +54,7 @@ describe("PascalCase", () => { assertType>("FooBarBazQux"); assertType>("FooBarBaz"); assertType>("FooBar"); + assertType>("FooBarBaz"); }); test("array", () => { @@ -72,6 +77,7 @@ describe("CamelCase", () => { assertType>("fooBaRb"); assertType>("fooBarBazQux"); assertType>("fooBar"); + assertType>("fooBarBaz"); }); test("array", () => { @@ -90,9 +96,59 @@ describe("JoinByCase", () => { assertType>("foo"); assertType>("foo-ba-rb"); assertType>("foo-bar-baz-qux"); + assertType>("foo-bar-baz"); }); test("array", () => { assertType>("foo-bar"); }); }); + +describe("TrainCase", () => { + test("types", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test("string", () => { + assertType>(""); + assertType>("F"); + assertType>("Foo"); + assertType>("Foo-B-Ar"); + assertType>("Accept-CH"); + assertType>("Foo-Bar-Baz-Qux"); + assertType>("FOO-BAR"); + // @ts-expect-error - splitByCase result may contain empty string + assertType>("Foo-Bar-Baz"); + assertType>("WWW-Authenticate"); + assertType>("WWW-Authenticate"); + assertType>("Foo-B-Ar"); + }); + + test("array", () => { + assertType>(""); + assertType>("Foo-Bar"); + }); +}); + +describe("TitleCase", () => { + test("string", () => { + assertType>(""); + assertType>("F"); + assertType>("Foo"); + assertType>("Foo B Ar"); + assertType>("Accept CH"); + assertType>("Foo Bar Baz Qux"); + assertType>("FOO BAR"); + // @ts-expect-error - splitByCase result may contain empty string + assertType>("Foo Bar Baz"); + assertType>("WWW Authenticate"); + assertType>("WWW Authenticate"); + assertType>("Foo B Ar"); + }); + + test("array", () => { + assertType>(""); + assertType>("Foo Bar"); + }); +});