diff --git a/packages/change-case/src/index.spec.ts b/packages/change-case/src/index.spec.ts index 87e9b5e3..e4a5c905 100644 --- a/packages/change-case/src/index.spec.ts +++ b/packages/change-case/src/index.spec.ts @@ -11,12 +11,12 @@ import { sentenceCase, snakeCase, split, + splitSeparateNumbers, trainCase, Options, } from "./index.js"; type Result = { - split: string[]; camelCase: string; capitalCase: string; constantCase: string; @@ -35,7 +35,6 @@ const tests: [string, Result, Options?][] = [ [ "", { - split: [], camelCase: "", capitalCase: "", constantCase: "", @@ -53,7 +52,6 @@ const tests: [string, Result, Options?][] = [ [ "test", { - split: ["test"], camelCase: "test", capitalCase: "Test", constantCase: "TEST", @@ -71,7 +69,6 @@ const tests: [string, Result, Options?][] = [ [ "test string", { - split: ["test", "string"], camelCase: "testString", capitalCase: "Test String", constantCase: "TEST_STRING", @@ -89,7 +86,6 @@ const tests: [string, Result, Options?][] = [ [ "Test String", { - split: ["Test", "String"], camelCase: "testString", capitalCase: "Test String", constantCase: "TEST_STRING", @@ -107,7 +103,6 @@ const tests: [string, Result, Options?][] = [ [ "Test String", { - split: ["Test", "String"], camelCase: "test$String", capitalCase: "Test$String", constantCase: "TEST$STRING", @@ -128,7 +123,6 @@ const tests: [string, Result, Options?][] = [ [ "TestV2", { - split: ["Test", "V2"], camelCase: "testV2", capitalCase: "Test V2", constantCase: "TEST_V2", @@ -146,7 +140,6 @@ const tests: [string, Result, Options?][] = [ [ "_foo_bar_", { - split: ["foo", "bar"], camelCase: "fooBar", capitalCase: "Foo Bar", constantCase: "FOO_BAR", @@ -164,7 +157,6 @@ const tests: [string, Result, Options?][] = [ [ "version 1.2.10", { - split: ["version", "1", "2", "10"], camelCase: "version_1_2_10", capitalCase: "Version 1 2 10", constantCase: "VERSION_1_2_10", @@ -182,7 +174,6 @@ const tests: [string, Result, Options?][] = [ [ "version 1.21.0", { - split: ["version", "1", "21", "0"], camelCase: "version_1_21_0", capitalCase: "Version 1 21 0", constantCase: "VERSION_1_21_0", @@ -200,7 +191,6 @@ const tests: [string, Result, Options?][] = [ [ "TestV2", { - split: ["Test", "V", "2"], camelCase: "testV_2", capitalCase: "Test V 2", constantCase: "TEST_V_2", @@ -214,12 +204,13 @@ const tests: [string, Result, Options?][] = [ snakeCase: "test_v_2", trainCase: "Test-V-2", }, - { separateNumbers: true }, + { + separateNumbers: true, + }, ], [ "1test", { - split: ["1", "test"], camelCase: "1Test", capitalCase: "1 Test", constantCase: "1_TEST", @@ -238,7 +229,6 @@ const tests: [string, Result, Options?][] = [ [ "Foo12019Bar", { - split: ["Foo", "12019", "Bar"], camelCase: "foo_12019Bar", capitalCase: "Foo 12019 Bar", constantCase: "FOO_12019_BAR", @@ -257,7 +247,6 @@ const tests: [string, Result, Options?][] = [ [ "aNumber2in", { - split: ["a", "Number", "2", "in"], camelCase: "aNumber_2In", capitalCase: "A Number 2 In", constantCase: "A_NUMBER_2_IN", @@ -276,7 +265,6 @@ const tests: [string, Result, Options?][] = [ [ "V1Test", { - split: ["V1", "Test"], camelCase: "v1Test", capitalCase: "V1 Test", constantCase: "V1_TEST", @@ -294,7 +282,6 @@ const tests: [string, Result, Options?][] = [ [ "V1Test with separateNumbers", { - split: ["V", "1", "Test", "with", "separate", "Numbers"], camelCase: "v_1TestWithSeparateNumbers", capitalCase: "V 1 Test With Separate Numbers", constantCase: "V_1_TEST_WITH_SEPARATE_NUMBERS", @@ -313,7 +300,6 @@ const tests: [string, Result, Options?][] = [ [ "__typename", { - split: ["typename"], camelCase: "__typename", capitalCase: "__Typename", constantCase: "__TYPENAME", @@ -334,7 +320,6 @@ const tests: [string, Result, Options?][] = [ [ "type__", { - split: ["type"], camelCase: "type__", capitalCase: "Type__", constantCase: "TYPE__", @@ -355,7 +340,6 @@ const tests: [string, Result, Options?][] = [ [ "__type__", { - split: ["type"], camelCase: "__type__", capitalCase: "__Type__", constantCase: "__TYPE__", @@ -379,7 +363,6 @@ const tests: [string, Result, Options?][] = [ describe("change case", () => { for (const [input, result, options] of tests) { it(input, () => { - expect(split(input, options)).toEqual(result.split); expect(camelCase(input, options)).toEqual(result.camelCase); expect(capitalCase(input, options)).toEqual(result.capitalCase); expect(constantCase(input, options)).toEqual(result.constantCase); @@ -394,6 +377,12 @@ describe("change case", () => { }); } + describe("split", () => { + it("should split an empty string", () => { + expect(split("")).toEqual([]); + }); + }); + describe("pascal case merge option", () => { it("should merge numbers", () => { const input = "version 1.2.10"; diff --git a/packages/change-case/src/index.ts b/packages/change-case/src/index.ts index e6235ed5..6bee8053 100644 --- a/packages/change-case/src/index.ts +++ b/packages/change-case/src/index.ts @@ -1,8 +1,9 @@ // Regexps involved with splitting words in various case formats. const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu; const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu; -const SPLIT_NUMBER_LOWER_RE = /(\d)(\p{Ll})/gu; -const SPLIT_LETTER_NUMBER_RE = /(\p{L})(\d)/gu; + +// Used to iterate over the initial split result and separate numbers. +const SPLIT_SEPARATE_NUMBER_RE = /(?<=\d)(\p{Ll})|(?<=\p{L})(\d)/u; // Regexp involved with stripping non-word characters from the result. const DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu; @@ -29,36 +30,26 @@ export interface PascalCaseOptions extends Options { /** * Options used for converting strings to any case. */ -export interface Options extends SplitOptions { +export interface Options { locale?: Locale; + split?: (value: string) => string[]; + /** @deprecated Pass `split: splitSeparateNumbers` instead. */ + separateNumbers?: boolean; delimiter?: string; prefixCharacters?: string; suffixCharacters?: string; } -/** - * Options used for splitting strings into word segments. - */ -export interface SplitOptions { - separateNumbers?: boolean; -} - /** * Split any cased input strings into an array of words. */ -export function split(value: string, options?: SplitOptions) { +export function split(value: string) { let result = value.trim(); result = result .replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE) .replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE); - if (options?.separateNumbers) { - result = result - .replace(SPLIT_NUMBER_LOWER_RE, SPLIT_REPLACE_VALUE) - .replace(SPLIT_LETTER_NUMBER_RE, SPLIT_REPLACE_VALUE); - } - result = result.replace(DEFAULT_STRIP_REGEXP, "\0"); let start = 0; @@ -72,16 +63,29 @@ export function split(value: string, options?: SplitOptions) { return result.slice(start, end).split(/\0/g); } +/** + * Split the input string into an array of words, separating numbers. + */ +export function splitSeparateNumbers(value: string) { + const words = split(value); + for (let i = 0; i < words.length; i++) { + const word = words[i]; + const match = SPLIT_SEPARATE_NUMBER_RE.exec(word); + if (match) { + words.splice(i, 1, word.slice(0, match.index), word.slice(match.index)); + } + } + return words; +} + /** * Convert a string to space separated lower case (`foo bar`). */ export function noCase(input: string, options?: Options) { - const [prefix, value, suffix] = splitPrefixSuffix(input, options); + const [prefix, words, suffix] = splitPrefixSuffix(input, options); return ( prefix + - split(value, options) - .map(lowerFactory(options?.locale)) - .join(options?.delimiter ?? " ") + + words.map(lowerFactory(options?.locale)).join(options?.delimiter ?? " ") + suffix ); } @@ -90,7 +94,7 @@ export function noCase(input: string, options?: Options) { * Convert a string to camel case (`fooBar`). */ export function camelCase(input: string, options?: PascalCaseOptions) { - const [prefix, value, suffix] = splitPrefixSuffix(input, options); + const [prefix, words, suffix] = splitPrefixSuffix(input, options); const lower = lowerFactory(options?.locale); const upper = upperFactory(options?.locale); const transform = options?.mergeAmbiguousCharacters @@ -98,7 +102,7 @@ export function camelCase(input: string, options?: PascalCaseOptions) { : pascalCaseTransformFactory(lower, upper); return ( prefix + - split(value, options) + words .map((word, index) => { if (index === 0) return lower(word); return transform(word, index); @@ -112,19 +116,13 @@ export function camelCase(input: string, options?: PascalCaseOptions) { * Convert a string to pascal case (`FooBar`). */ export function pascalCase(input: string, options?: PascalCaseOptions) { - const [prefix, value, suffix] = splitPrefixSuffix(input, options); + const [prefix, words, suffix] = splitPrefixSuffix(input, options); const lower = lowerFactory(options?.locale); const upper = upperFactory(options?.locale); const transform = options?.mergeAmbiguousCharacters ? capitalCaseTransformFactory(lower, upper) : pascalCaseTransformFactory(lower, upper); - return ( - prefix + - split(value, options) - .map(transform) - .join(options?.delimiter ?? "") + - suffix - ); + return prefix + words.map(transform).join(options?.delimiter ?? "") + suffix; } /** @@ -138,12 +136,12 @@ export function pascalSnakeCase(input: string, options?: Options) { * Convert a string to capital case (`Foo Bar`). */ export function capitalCase(input: string, options?: Options) { - const [prefix, value, suffix] = splitPrefixSuffix(input, options); + const [prefix, words, suffix] = splitPrefixSuffix(input, options); const lower = lowerFactory(options?.locale); const upper = upperFactory(options?.locale); return ( prefix + - split(value, options) + words .map(capitalCaseTransformFactory(lower, upper)) .join(options?.delimiter ?? " ") + suffix @@ -154,12 +152,10 @@ export function capitalCase(input: string, options?: Options) { * Convert a string to constant case (`FOO_BAR`). */ export function constantCase(input: string, options?: Options) { - const [prefix, value, suffix] = splitPrefixSuffix(input, options); + const [prefix, words, suffix] = splitPrefixSuffix(input, options); return ( prefix + - split(value, options) - .map(upperFactory(options?.locale)) - .join(options?.delimiter ?? "_") + + words.map(upperFactory(options?.locale)).join(options?.delimiter ?? "_") + suffix ); } @@ -189,13 +185,13 @@ export function pathCase(input: string, options?: Options) { * Convert a string to path case (`Foo bar`). */ export function sentenceCase(input: string, options?: Options) { - const [prefix, value, suffix] = splitPrefixSuffix(input, options); + const [prefix, words, suffix] = splitPrefixSuffix(input, options); const lower = lowerFactory(options?.locale); const upper = upperFactory(options?.locale); const transform = capitalCaseTransformFactory(lower, upper); return ( prefix + - split(value, options) + words .map((word, index) => { if (index === 0) return transform(word); return lower(word); @@ -252,12 +248,14 @@ function pascalCaseTransformFactory( function splitPrefixSuffix( input: string, - options: Options | undefined, -): [string, string, string] { + options: Options = {}, +): [string, string[], string] { + const splitFn = + options.split ?? (options.separateNumbers ? splitSeparateNumbers : split); const prefixCharacters = - options?.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; + options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; const suffixCharacters = - options?.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; + options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; let prefixIndex = 0; let suffixIndex = input.length; @@ -276,7 +274,7 @@ function splitPrefixSuffix( return [ input.slice(0, prefixIndex), - input.slice(prefixIndex, suffixIndex), + splitFn(input.slice(prefixIndex, suffixIndex)), input.slice(suffixIndex), ]; }