-
-
Notifications
You must be signed in to change notification settings - Fork 548
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
Improve DelimiterCase
#930
base: main
Are you sure you want to change the base?
Changes from all commits
75eb608
3c45425
22605ec
f97b734
e4b7b9a
004cfbd
f760698
6e87231
0580a3d
5a8b562
e983b73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,20 @@ | ||
import type {UpperCaseCharacters, WordSeparators} from './internal'; | ||
|
||
// Transforms a string that is fully uppercase into a fully lowercase version. Needed to add support for SCREAMING_SNAKE_CASE, see https://github.com/sindresorhus/type-fest/issues/385 | ||
type UpperCaseToLowerCase<T extends string> = T extends Uppercase<T> ? Lowercase<T> : T; | ||
|
||
// This implementation does not support SCREAMING_SNAKE_CASE, it is used internally by `SplitIncludingDelimiters`. | ||
type SplitIncludingDelimiters_<Source extends string, Delimiter extends string> = | ||
Source extends '' ? [] : | ||
Source extends `${infer FirstPart}${Delimiter}${infer SecondPart}` ? | ||
( | ||
Source extends `${FirstPart}${infer UsedDelimiter}${SecondPart}` | ||
? UsedDelimiter extends Delimiter | ||
? Source extends `${infer FirstPart}${UsedDelimiter}${infer SecondPart}` | ||
? [...SplitIncludingDelimiters<FirstPart, Delimiter>, UsedDelimiter, ...SplitIncludingDelimiters<SecondPart, Delimiter>] | ||
: never | ||
: never | ||
: never | ||
) : | ||
[Source]; | ||
|
||
/** | ||
Unlike a simpler split, this one includes the delimiter splitted on in the resulting array literal. This is to enable splitting on, for example, upper-case characters. | ||
|
||
@category Template literal | ||
*/ | ||
export type SplitIncludingDelimiters<Source extends string, Delimiter extends string> = SplitIncludingDelimiters_<UpperCaseToLowerCase<Source>, Delimiter>; | ||
import type {SplitWords, SplitWordsOptions} from './split-words'; | ||
|
||
/** | ||
Format a specific part of the splitted string literal that `StringArrayToDelimiterCase<>` fuses together, ensuring desired casing. | ||
|
||
@see StringArrayToDelimiterCase | ||
Convert an array of words to delimiter case starting with a delimiter with input capitalization. | ||
*/ | ||
type StringPartToDelimiterCase<StringPart extends string, Start extends boolean, UsedWordSeparators extends string, UsedUpperCaseCharacters extends string, Delimiter extends string> = | ||
StringPart extends UsedWordSeparators ? Delimiter : | ||
Start extends true ? Lowercase<StringPart> : | ||
StringPart extends UsedUpperCaseCharacters ? `${Delimiter}${Lowercase<StringPart>}` : | ||
StringPart; | ||
|
||
/** | ||
Takes the result of a splitted string literal and recursively concatenates it together into the desired casing. | ||
|
||
It receives `UsedWordSeparators` and `UsedUpperCaseCharacters` as input to ensure it's fully encapsulated. | ||
|
||
@see SplitIncludingDelimiters | ||
*/ | ||
type StringArrayToDelimiterCase<Parts extends readonly any[], Start extends boolean, UsedWordSeparators extends string, UsedUpperCaseCharacters extends string, Delimiter extends string> = | ||
Parts extends [`${infer FirstPart}`, ...infer RemainingParts] | ||
? `${StringPartToDelimiterCase<FirstPart, Start, UsedWordSeparators, UsedUpperCaseCharacters, Delimiter>}${StringArrayToDelimiterCase<RemainingParts, false, UsedWordSeparators, UsedUpperCaseCharacters, Delimiter>}` | ||
: Parts extends [string] | ||
? string | ||
: ''; | ||
type DelimiterCaseFromArray< | ||
Words extends string[], | ||
Delimiter extends string, | ||
OutputString extends string = '', | ||
> = Words extends [ | ||
infer FirstWord extends string, | ||
...infer RemainingWords extends string[], | ||
] | ||
? `${Delimiter}${FirstWord}${DelimiterCaseFromArray<RemainingWords, Delimiter>}` | ||
: OutputString; | ||
|
||
type RemoveFirstLetter<S extends string> = S extends `${infer _}${infer Rest}` ? Rest : ''; | ||
|
||
/** | ||
Convert a string literal to a custom string delimiter casing. | ||
|
@@ -65,6 +31,7 @@ import type {DelimiterCase} from 'type-fest'; | |
// Simple | ||
|
||
const someVariable: DelimiterCase<'fooBar', '#'> = 'foo#bar'; | ||
const someVariableNoSplitOnNumber: DelimiterCase<'p2pNetwork', '#', {splitOnNumber: false}> = 'p2p#network'; | ||
|
||
// Advanced | ||
|
||
|
@@ -87,13 +54,9 @@ const rawCliOptions: OddlyCasedProperties<SomeOptions> = { | |
|
||
@category Change case | ||
@category Template literal | ||
*/ | ||
export type DelimiterCase<Value, Delimiter extends string> = string extends Value ? Value : Value extends string | ||
? StringArrayToDelimiterCase< | ||
SplitIncludingDelimiters<Value, WordSeparators | UpperCaseCharacters>, | ||
true, | ||
WordSeparators, | ||
UpperCaseCharacters, | ||
Delimiter | ||
> | ||
*/ | ||
export type DelimiterCase<Value, Delimiter extends string, Options extends SplitWordsOptions = {splitOnNumber: true}> = Value extends string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we
|
||
? string extends Value | ||
? Value | ||
: Lowercase<RemoveFirstLetter<DelimiterCaseFromArray<SplitWords<Value, Options>, Delimiter>>> | ||
: Value; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import type {DelimiterCase} from './delimiter-case'; | ||
import type {SplitWordsOptions} from './split-words'; | ||
|
||
/** | ||
Convert a string literal to kebab-case. | ||
|
@@ -12,6 +13,7 @@ import type {KebabCase} from 'type-fest'; | |
// Simple | ||
|
||
const someVariable: KebabCase<'fooBar'> = 'foo-bar'; | ||
const someVariableNoSplitOnNumber: KebabCase<'p2pNetwork', {splitOnNumber: false}> = 'p2p-network'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this new option on case changing types |
||
|
||
// Advanced | ||
|
||
|
@@ -35,4 +37,4 @@ const rawCliOptions: KebabCasedProperties<CliOptions> = { | |
@category Change case | ||
@category Template literal | ||
*/ | ||
export type KebabCase<Value> = DelimiterCase<Value, '-'>; | ||
export type KebabCase<Value, Options extends SplitWordsOptions = {splitOnNumber: true}> = DelimiterCase<Value, '-', Options>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,5 @@ | ||
import type {SplitIncludingDelimiters} from './delimiter-case'; | ||
import type {SnakeCase} from './snake-case'; | ||
import type {Includes} from './includes'; | ||
|
||
/** | ||
Returns a boolean for whether the string is screaming snake case. | ||
*/ | ||
type IsScreamingSnakeCase<Value extends string> = Value extends Uppercase<Value> | ||
? Includes<SplitIncludingDelimiters<Lowercase<Value>, '_'>, '_'> extends true | ||
? true | ||
: false | ||
: false; | ||
import type {SplitWordsOptions} from './split-words'; | ||
|
||
/** | ||
Convert a string literal to screaming-snake-case. | ||
|
@@ -21,13 +11,13 @@ This can be useful when, for example, converting a camel-cased object property t | |
import type {ScreamingSnakeCase} from 'type-fest'; | ||
|
||
const someVariable: ScreamingSnakeCase<'fooBar'> = 'FOO_BAR'; | ||
const someVariableNoSplitOnNumber: ScreamingSnakeCase<'p2pNetwork', {splitOnNumber: false}> = 'P2P_NETWORK'; | ||
|
||
``` | ||
|
||
@category Change case | ||
@category Template literal | ||
*/ | ||
export type ScreamingSnakeCase<Value> = Value extends string | ||
? IsScreamingSnakeCase<Value> extends true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can bring this back, but I believe it shouldn't be necessary, especially that we don't use it for other things like snake case or kebab case |
||
? Value | ||
: Uppercase<SnakeCase<Value>> | ||
*/ | ||
export type ScreamingSnakeCase<Value, Options extends SplitWordsOptions = {splitOnNumber: true}> = Value extends string | ||
? Uppercase<SnakeCase<Value, Options>> | ||
: Value; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ type RemoveLastCharacter<Sentence extends string, Character extends string> = Se | |
? SkipEmptyWord<LeftSide> | ||
: never; | ||
|
||
export type SplitWordsOptions = {splitOnNumber: boolean}; | ||
/** | ||
Split a string (almost) like Lodash's `_.words()` function. | ||
|
||
|
@@ -20,6 +21,7 @@ type Words1 = SplitWords<'helloWORLD'>; // ['hello', 'WORLD'] | |
type Words2 = SplitWords<'hello-world'>; // ['hello', 'world'] | ||
type Words3 = SplitWords<'--hello the_world'>; // ['hello', 'the', 'world'] | ||
type Words4 = SplitWords<'lifeIs42'>; // ['life', 'Is', '42'] | ||
type Words5 = SplitWords<'p2pNetwork', { splitOnNumber: false }>; // ['p2p', 'Network'] | ||
``` | ||
|
||
@internal | ||
|
@@ -28,6 +30,7 @@ type Words4 = SplitWords<'lifeIs42'>; // ['life', 'Is', '42'] | |
*/ | ||
export type SplitWords< | ||
Sentence extends string, | ||
Options extends SplitWordsOptions = {splitOnNumber: true}, | ||
LastCharacter extends string = '', | ||
CurrentWord extends string = '', | ||
> = Sentence extends `${infer FirstCharacter}${infer RemainingCharacters}` | ||
|
@@ -36,22 +39,30 @@ export type SplitWords< | |
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters>] | ||
: LastCharacter extends '' | ||
// Fist char of word | ||
? SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter> | ||
// Case change: non-numeric to numeric, push word | ||
? SplitWords<RemainingCharacters, Options, FirstCharacter, FirstCharacter> | ||
// Case change: non-numeric to numeric | ||
: [false, true] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>] | ||
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>] | ||
// Case change: numeric to non-numeric, push word | ||
// Split on number: push word | ||
? Options['splitOnNumber'] extends true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When there's a numeric/non-numeric case change I added and used a new option
|
||
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, Options, FirstCharacter, FirstCharacter>] | ||
// No split on number: concat word | ||
: SplitWords<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`> | ||
// Case change: numeric to non-numeric | ||
: [true, false] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>] | ||
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>] | ||
// Split on number: push word | ||
? Options['splitOnNumber'] extends true | ||
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, Options, FirstCharacter, FirstCharacter>] | ||
// No split on number: concat word | ||
: SplitWords<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`> | ||
// No case change: concat word | ||
: [true, true] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>] | ||
? SplitWords<RemainingCharacters, FirstCharacter, `${CurrentWord}${FirstCharacter}`> | ||
// Case change: lower to upper, push word | ||
? SplitWords<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`> | ||
// Case change: lower to upper, push word | ||
: [true, true] extends [IsLowerCase<LastCharacter>, IsUpperCase<FirstCharacter>] | ||
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>] | ||
// Case change: upper to lower, brings back the last character, push word | ||
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, Options, FirstCharacter, FirstCharacter>] | ||
// Case change: upper to lower, brings back the last character, push word | ||
: [true, true] extends [IsUpperCase<LastCharacter>, IsLowerCase<FirstCharacter>] | ||
? [...RemoveLastCharacter<CurrentWord, LastCharacter>, ...SplitWords<RemainingCharacters, FirstCharacter, `${LastCharacter}${FirstCharacter}`>] | ||
// No case change: concat word | ||
: SplitWords<RemainingCharacters, FirstCharacter, `${CurrentWord}${FirstCharacter}`> | ||
? [...RemoveLastCharacter<CurrentWord, LastCharacter>, ...SplitWords<RemainingCharacters, Options, FirstCharacter, `${LastCharacter}${FirstCharacter}`>] | ||
// No case change: concat word | ||
: SplitWords<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`> | ||
: [...SkipEmptyWord<CurrentWord>]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gets the words split and inserts the delimited at the beginning and after every word
Like
['here', 'We', 'Go']
->'#here#We#Go'