deep-case-crafter 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -13,28 +13,25 @@ interface TransformOptions {
13
13
  */
14
14
  targetCase?: StringCase;
15
15
  /**
16
- * The source casing format to transform from
16
+ * The source casing format.
17
17
  *
18
- * When specified:
19
- * - Provides precise TypeScript type transformations
20
- * - Bypasses runtime case detection (more efficient)
21
- * - Ensures consistent results if input has mixed casing
18
+ * This is a **type-level assertion only**, used (together with `targetCase`) to
19
+ * produce precise output types. It does **not** change runtime behavior: at runtime
20
+ * every key's case is auto-detected individually whether or not `sourceCase` is set.
22
21
  *
23
- * When omitted:
24
- * - Automatic case detection is used at runtime
25
- * - TypeScript types will be less precise (string keys)
26
- * - Works with mixed case inputs (each key detected separately)
22
+ * Because of that, the precise types are accurate only for keys that are genuinely
23
+ * well-formed in the declared source case. For ambiguous keys — mixed case
24
+ * (`mixed_Case`), SCREAMING_SNAKE (`FOO_BAR`), mixed separators (`a_b-c`), or
25
+ * non-ASCII (`café_table`) — the runtime preserves the key unchanged, while the type
26
+ * may show it transformed. Set `sourceCase` only when your keys really are that case.
27
+ *
28
+ * When omitted, the result type is `unknown` (the runtime still auto-detects each key).
27
29
  *
28
30
  * @example
29
- * // With source specified - precise types
31
+ * // Precise types when both are set and keys are well-formed snake_case:
30
32
  * const result = transform(data, { targetCase: 'camel', sourceCase: 'snake' });
31
- * // result has exact transformed types
32
- *
33
- * // Without source - flexible runtime behavior
34
- * const result = transform(data, { targetCase: 'camel' });
35
- * // TypeScript types are more general
36
33
  *
37
- * @default undefined (automatic detection)
34
+ * @default undefined
38
35
  */
39
36
  sourceCase?: StringCase;
40
37
  }
@@ -60,37 +57,56 @@ type SnakeToPascal<S extends string> = S extends `${infer P}_${infer R}` ? Snake
60
57
  */
61
58
  type SnakeToKebab<S extends string> = S extends `${infer P}_${infer R}` ? `${P}-${SnakeToKebab<R>}` : S;
62
59
  /**
63
- * Type utility that transforms a camelCase string to snake_case
64
- * Example: 'helloWorld' -> 'hello_world'
60
+ * Acronym-aware word-boundary scanner shared by all camel/pascal → snake/kebab
61
+ * conversions. Mirrors `utils/stringFormatters/toSeparatedCase.ts` exactly so the
62
+ * type-level and runtime results never diverge.
63
+ *
64
+ * Inserts `Sep` before an uppercase char when the previous char is a lowercase
65
+ * letter or digit, or when it ends an acronym run that starts a new word (the two
66
+ * preceding chars are uppercase and the next is lowercase). All output is lowercased.
67
+ */
68
+ type IsUpper<C extends string> = C extends Uppercase<C> ? (C extends Lowercase<C> ? false : true) : false;
69
+ type IsLower<C extends string> = C extends Lowercase<C> ? (C extends Uppercase<C> ? false : true) : false;
70
+ type IsLowerOrDigit<C extends string> = IsLower<C> extends true ? true : C extends `${number}` ? true : false;
71
+ type FirstChar<S extends string> = S extends `${infer F}${string}` ? F : '';
72
+ /**
73
+ * Whether an uppercase `C` at the current position begins a new word (so a
74
+ * separator goes before it): true when the previous char is lower/digit, or when
75
+ * `C` ends an acronym run (two preceding uppercase chars) followed by a lowercase.
65
76
  */
66
- type CamelToSnake<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `_${Lowercase<C>}${CamelToSnake<R>}` : `${C}${CamelToSnake<R>}` : S;
77
+ type NeedsSeparator<C extends string, Prev extends string, PrevPrev extends string, Next extends string> = IsUpper<C> extends true ? IsLowerOrDigit<Prev> extends true ? true : IsUpper<Prev> extends true ? IsUpper<PrevPrev> extends true ? IsLower<Next> : false : false : false;
78
+ type SeparatorScan<S extends string, Sep extends string, Prev extends string = '', PrevPrev extends string = '', Acc extends string = ''> = S extends `${infer C}${infer R}` ? SeparatorScan<R, Sep, C, Prev, `${Acc}${NeedsSeparator<C, Prev, PrevPrev, FirstChar<R>> extends true ? Sep : ''}${Lowercase<C>}`> : Acc;
79
+ type SeparatorCase<S extends string, Sep extends string> = SeparatorScan<S, Sep>;
80
+ /**
81
+ * Type utility that transforms a camelCase string to snake_case (acronym-aware)
82
+ * Example: 'helloWorld' -> 'hello_world', 'getHTTPResponse' -> 'get_http_response'
83
+ */
84
+ type CamelToSnake<S extends string> = SeparatorCase<S, '_'>;
67
85
  /**
68
86
  * Type utility that transforms a camelCase string to PascalCase
69
87
  * Example: 'helloWorld' -> 'HelloWorld'
70
88
  */
71
89
  type CamelToPascal<S extends string> = Capitalize<S>;
72
90
  /**
73
- * Type utility that transforms a camelCase string to kebab-case
74
- * Example: 'helloWorld' -> 'hello-world'
91
+ * Type utility that transforms a camelCase string to kebab-case (acronym-aware)
92
+ * Example: 'helloWorld' -> 'hello-world', 'getHTTPResponse' -> 'get-http-response'
75
93
  */
76
- type CamelToKebab<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `-${Lowercase<C>}${CamelToKebab<R>}` : `${C}${CamelToKebab<R>}` : S;
94
+ type CamelToKebab<S extends string> = SeparatorCase<S, '-'>;
77
95
  /**
78
96
  * Type utility that transforms a PascalCase string to camelCase
79
97
  * Example: 'HelloWorld' -> 'helloWorld'
80
98
  */
81
99
  type PascalToCamel<S extends string> = S extends `${infer F}${infer R}` ? `${Lowercase<F>}${R}` : S;
82
100
  /**
83
- * Type utility that transforms a PascalCase string to snake_case
84
- * Example: 'HelloWorld' -> 'hello_world'
101
+ * Type utility that transforms a PascalCase string to snake_case (acronym-aware)
102
+ * Example: 'HelloWorld' -> 'hello_world', 'HTTPResponse' -> 'http_response'
85
103
  */
86
- type PascalToSnake<S extends string> = S extends `${infer F}${infer R}` ? R extends `` ? Lowercase<F> : `${Lowercase<F>}${PascalToSnakeInner<R>}` : S;
87
- type PascalToSnakeInner<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `_${Lowercase<C>}${PascalToSnakeInner<R>}` : `${C}${PascalToSnakeInner<R>}` : S;
104
+ type PascalToSnake<S extends string> = SeparatorCase<S, '_'>;
88
105
  /**
89
- * Type utility that transforms a PascalCase string to kebab-case
90
- * Example: 'HelloWorld' -> 'hello-world'
106
+ * Type utility that transforms a PascalCase string to kebab-case (acronym-aware)
107
+ * Example: 'HelloWorld' -> 'hello-world', 'HTTPResponse' -> 'http-response'
91
108
  */
92
- type PascalToKebab<S extends string> = S extends `${infer F}${infer R}` ? R extends `` ? Lowercase<F> : `${Lowercase<F>}${PascalToKebabInner<R>}` : S;
93
- type PascalToKebabInner<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `-${Lowercase<C>}${PascalToKebabInner<R>}` : `${C}${PascalToKebabInner<R>}` : S;
109
+ type PascalToKebab<S extends string> = SeparatorCase<S, '-'>;
94
110
  /**
95
111
  * Type utility that transforms a kebab-case string to camelCase
96
112
  * Example: 'hello-world' -> 'helloWorld'
@@ -122,7 +138,20 @@ type KebabToSnake<S extends string> = S extends `${infer P}-${infer R}` ? `${P}_
122
138
  */
123
139
  type ForbiddenStartChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
124
140
  type ForbiddenEndChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
125
- type PreservedKey<K extends string> = K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
141
+ /**
142
+ * The character set the runtime is willing to transform. Any key containing a
143
+ * character outside it — a symbol, or a non-ASCII letter like `é` — is preserved
144
+ * wholesale at runtime (`/[^A-Za-z0-9_-]/`), so the type must preserve it too.
145
+ * An explicit ASCII set is required because TS's `Uppercase`/`Lowercase` are
146
+ * Unicode-aware and would otherwise treat `é` as an ordinary transformable letter.
147
+ */
148
+ type AllowedKeyChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '_' | '-';
149
+ /**
150
+ * True if `S` contains any character outside `AllowedKeyChar`. Mirrors the runtime's
151
+ * `/[^A-Za-z0-9_-]/` preserve check.
152
+ */
153
+ type HasOutOfCharsetChar<S extends string> = S extends `${infer C}${infer R}` ? C extends AllowedKeyChar ? HasOutOfCharsetChar<R> : true : false;
154
+ type PreservedKey<K extends string> = HasOutOfCharsetChar<K> extends true ? true : K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
126
155
  /**
127
156
  * Type utility that selects the correct string transformation based on source and target casing
128
157
  */
package/dist/index.d.mts CHANGED
@@ -13,28 +13,25 @@ interface TransformOptions {
13
13
  */
14
14
  targetCase?: StringCase;
15
15
  /**
16
- * The source casing format to transform from
16
+ * The source casing format.
17
17
  *
18
- * When specified:
19
- * - Provides precise TypeScript type transformations
20
- * - Bypasses runtime case detection (more efficient)
21
- * - Ensures consistent results if input has mixed casing
18
+ * This is a **type-level assertion only**, used (together with `targetCase`) to
19
+ * produce precise output types. It does **not** change runtime behavior: at runtime
20
+ * every key's case is auto-detected individually whether or not `sourceCase` is set.
22
21
  *
23
- * When omitted:
24
- * - Automatic case detection is used at runtime
25
- * - TypeScript types will be less precise (string keys)
26
- * - Works with mixed case inputs (each key detected separately)
22
+ * Because of that, the precise types are accurate only for keys that are genuinely
23
+ * well-formed in the declared source case. For ambiguous keys — mixed case
24
+ * (`mixed_Case`), SCREAMING_SNAKE (`FOO_BAR`), mixed separators (`a_b-c`), or
25
+ * non-ASCII (`café_table`) — the runtime preserves the key unchanged, while the type
26
+ * may show it transformed. Set `sourceCase` only when your keys really are that case.
27
+ *
28
+ * When omitted, the result type is `unknown` (the runtime still auto-detects each key).
27
29
  *
28
30
  * @example
29
- * // With source specified - precise types
31
+ * // Precise types when both are set and keys are well-formed snake_case:
30
32
  * const result = transform(data, { targetCase: 'camel', sourceCase: 'snake' });
31
- * // result has exact transformed types
32
- *
33
- * // Without source - flexible runtime behavior
34
- * const result = transform(data, { targetCase: 'camel' });
35
- * // TypeScript types are more general
36
33
  *
37
- * @default undefined (automatic detection)
34
+ * @default undefined
38
35
  */
39
36
  sourceCase?: StringCase;
40
37
  }
@@ -60,37 +57,56 @@ type SnakeToPascal<S extends string> = S extends `${infer P}_${infer R}` ? Snake
60
57
  */
61
58
  type SnakeToKebab<S extends string> = S extends `${infer P}_${infer R}` ? `${P}-${SnakeToKebab<R>}` : S;
62
59
  /**
63
- * Type utility that transforms a camelCase string to snake_case
64
- * Example: 'helloWorld' -> 'hello_world'
60
+ * Acronym-aware word-boundary scanner shared by all camel/pascal → snake/kebab
61
+ * conversions. Mirrors `utils/stringFormatters/toSeparatedCase.ts` exactly so the
62
+ * type-level and runtime results never diverge.
63
+ *
64
+ * Inserts `Sep` before an uppercase char when the previous char is a lowercase
65
+ * letter or digit, or when it ends an acronym run that starts a new word (the two
66
+ * preceding chars are uppercase and the next is lowercase). All output is lowercased.
67
+ */
68
+ type IsUpper<C extends string> = C extends Uppercase<C> ? (C extends Lowercase<C> ? false : true) : false;
69
+ type IsLower<C extends string> = C extends Lowercase<C> ? (C extends Uppercase<C> ? false : true) : false;
70
+ type IsLowerOrDigit<C extends string> = IsLower<C> extends true ? true : C extends `${number}` ? true : false;
71
+ type FirstChar<S extends string> = S extends `${infer F}${string}` ? F : '';
72
+ /**
73
+ * Whether an uppercase `C` at the current position begins a new word (so a
74
+ * separator goes before it): true when the previous char is lower/digit, or when
75
+ * `C` ends an acronym run (two preceding uppercase chars) followed by a lowercase.
65
76
  */
66
- type CamelToSnake<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `_${Lowercase<C>}${CamelToSnake<R>}` : `${C}${CamelToSnake<R>}` : S;
77
+ type NeedsSeparator<C extends string, Prev extends string, PrevPrev extends string, Next extends string> = IsUpper<C> extends true ? IsLowerOrDigit<Prev> extends true ? true : IsUpper<Prev> extends true ? IsUpper<PrevPrev> extends true ? IsLower<Next> : false : false : false;
78
+ type SeparatorScan<S extends string, Sep extends string, Prev extends string = '', PrevPrev extends string = '', Acc extends string = ''> = S extends `${infer C}${infer R}` ? SeparatorScan<R, Sep, C, Prev, `${Acc}${NeedsSeparator<C, Prev, PrevPrev, FirstChar<R>> extends true ? Sep : ''}${Lowercase<C>}`> : Acc;
79
+ type SeparatorCase<S extends string, Sep extends string> = SeparatorScan<S, Sep>;
80
+ /**
81
+ * Type utility that transforms a camelCase string to snake_case (acronym-aware)
82
+ * Example: 'helloWorld' -> 'hello_world', 'getHTTPResponse' -> 'get_http_response'
83
+ */
84
+ type CamelToSnake<S extends string> = SeparatorCase<S, '_'>;
67
85
  /**
68
86
  * Type utility that transforms a camelCase string to PascalCase
69
87
  * Example: 'helloWorld' -> 'HelloWorld'
70
88
  */
71
89
  type CamelToPascal<S extends string> = Capitalize<S>;
72
90
  /**
73
- * Type utility that transforms a camelCase string to kebab-case
74
- * Example: 'helloWorld' -> 'hello-world'
91
+ * Type utility that transforms a camelCase string to kebab-case (acronym-aware)
92
+ * Example: 'helloWorld' -> 'hello-world', 'getHTTPResponse' -> 'get-http-response'
75
93
  */
76
- type CamelToKebab<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `-${Lowercase<C>}${CamelToKebab<R>}` : `${C}${CamelToKebab<R>}` : S;
94
+ type CamelToKebab<S extends string> = SeparatorCase<S, '-'>;
77
95
  /**
78
96
  * Type utility that transforms a PascalCase string to camelCase
79
97
  * Example: 'HelloWorld' -> 'helloWorld'
80
98
  */
81
99
  type PascalToCamel<S extends string> = S extends `${infer F}${infer R}` ? `${Lowercase<F>}${R}` : S;
82
100
  /**
83
- * Type utility that transforms a PascalCase string to snake_case
84
- * Example: 'HelloWorld' -> 'hello_world'
101
+ * Type utility that transforms a PascalCase string to snake_case (acronym-aware)
102
+ * Example: 'HelloWorld' -> 'hello_world', 'HTTPResponse' -> 'http_response'
85
103
  */
86
- type PascalToSnake<S extends string> = S extends `${infer F}${infer R}` ? R extends `` ? Lowercase<F> : `${Lowercase<F>}${PascalToSnakeInner<R>}` : S;
87
- type PascalToSnakeInner<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `_${Lowercase<C>}${PascalToSnakeInner<R>}` : `${C}${PascalToSnakeInner<R>}` : S;
104
+ type PascalToSnake<S extends string> = SeparatorCase<S, '_'>;
88
105
  /**
89
- * Type utility that transforms a PascalCase string to kebab-case
90
- * Example: 'HelloWorld' -> 'hello-world'
106
+ * Type utility that transforms a PascalCase string to kebab-case (acronym-aware)
107
+ * Example: 'HelloWorld' -> 'hello-world', 'HTTPResponse' -> 'http-response'
91
108
  */
92
- type PascalToKebab<S extends string> = S extends `${infer F}${infer R}` ? R extends `` ? Lowercase<F> : `${Lowercase<F>}${PascalToKebabInner<R>}` : S;
93
- type PascalToKebabInner<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `-${Lowercase<C>}${PascalToKebabInner<R>}` : `${C}${PascalToKebabInner<R>}` : S;
109
+ type PascalToKebab<S extends string> = SeparatorCase<S, '-'>;
94
110
  /**
95
111
  * Type utility that transforms a kebab-case string to camelCase
96
112
  * Example: 'hello-world' -> 'helloWorld'
@@ -122,7 +138,20 @@ type KebabToSnake<S extends string> = S extends `${infer P}-${infer R}` ? `${P}_
122
138
  */
123
139
  type ForbiddenStartChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
124
140
  type ForbiddenEndChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
125
- type PreservedKey<K extends string> = K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
141
+ /**
142
+ * The character set the runtime is willing to transform. Any key containing a
143
+ * character outside it — a symbol, or a non-ASCII letter like `é` — is preserved
144
+ * wholesale at runtime (`/[^A-Za-z0-9_-]/`), so the type must preserve it too.
145
+ * An explicit ASCII set is required because TS's `Uppercase`/`Lowercase` are
146
+ * Unicode-aware and would otherwise treat `é` as an ordinary transformable letter.
147
+ */
148
+ type AllowedKeyChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '_' | '-';
149
+ /**
150
+ * True if `S` contains any character outside `AllowedKeyChar`. Mirrors the runtime's
151
+ * `/[^A-Za-z0-9_-]/` preserve check.
152
+ */
153
+ type HasOutOfCharsetChar<S extends string> = S extends `${infer C}${infer R}` ? C extends AllowedKeyChar ? HasOutOfCharsetChar<R> : true : false;
154
+ type PreservedKey<K extends string> = HasOutOfCharsetChar<K> extends true ? true : K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
126
155
  /**
127
156
  * Type utility that selects the correct string transformation based on source and target casing
128
157
  */
package/dist/index.d.ts CHANGED
@@ -13,28 +13,25 @@ interface TransformOptions {
13
13
  */
14
14
  targetCase?: StringCase;
15
15
  /**
16
- * The source casing format to transform from
16
+ * The source casing format.
17
17
  *
18
- * When specified:
19
- * - Provides precise TypeScript type transformations
20
- * - Bypasses runtime case detection (more efficient)
21
- * - Ensures consistent results if input has mixed casing
18
+ * This is a **type-level assertion only**, used (together with `targetCase`) to
19
+ * produce precise output types. It does **not** change runtime behavior: at runtime
20
+ * every key's case is auto-detected individually whether or not `sourceCase` is set.
22
21
  *
23
- * When omitted:
24
- * - Automatic case detection is used at runtime
25
- * - TypeScript types will be less precise (string keys)
26
- * - Works with mixed case inputs (each key detected separately)
22
+ * Because of that, the precise types are accurate only for keys that are genuinely
23
+ * well-formed in the declared source case. For ambiguous keys — mixed case
24
+ * (`mixed_Case`), SCREAMING_SNAKE (`FOO_BAR`), mixed separators (`a_b-c`), or
25
+ * non-ASCII (`café_table`) — the runtime preserves the key unchanged, while the type
26
+ * may show it transformed. Set `sourceCase` only when your keys really are that case.
27
+ *
28
+ * When omitted, the result type is `unknown` (the runtime still auto-detects each key).
27
29
  *
28
30
  * @example
29
- * // With source specified - precise types
31
+ * // Precise types when both are set and keys are well-formed snake_case:
30
32
  * const result = transform(data, { targetCase: 'camel', sourceCase: 'snake' });
31
- * // result has exact transformed types
32
- *
33
- * // Without source - flexible runtime behavior
34
- * const result = transform(data, { targetCase: 'camel' });
35
- * // TypeScript types are more general
36
33
  *
37
- * @default undefined (automatic detection)
34
+ * @default undefined
38
35
  */
39
36
  sourceCase?: StringCase;
40
37
  }
@@ -60,37 +57,56 @@ type SnakeToPascal<S extends string> = S extends `${infer P}_${infer R}` ? Snake
60
57
  */
61
58
  type SnakeToKebab<S extends string> = S extends `${infer P}_${infer R}` ? `${P}-${SnakeToKebab<R>}` : S;
62
59
  /**
63
- * Type utility that transforms a camelCase string to snake_case
64
- * Example: 'helloWorld' -> 'hello_world'
60
+ * Acronym-aware word-boundary scanner shared by all camel/pascal → snake/kebab
61
+ * conversions. Mirrors `utils/stringFormatters/toSeparatedCase.ts` exactly so the
62
+ * type-level and runtime results never diverge.
63
+ *
64
+ * Inserts `Sep` before an uppercase char when the previous char is a lowercase
65
+ * letter or digit, or when it ends an acronym run that starts a new word (the two
66
+ * preceding chars are uppercase and the next is lowercase). All output is lowercased.
67
+ */
68
+ type IsUpper<C extends string> = C extends Uppercase<C> ? (C extends Lowercase<C> ? false : true) : false;
69
+ type IsLower<C extends string> = C extends Lowercase<C> ? (C extends Uppercase<C> ? false : true) : false;
70
+ type IsLowerOrDigit<C extends string> = IsLower<C> extends true ? true : C extends `${number}` ? true : false;
71
+ type FirstChar<S extends string> = S extends `${infer F}${string}` ? F : '';
72
+ /**
73
+ * Whether an uppercase `C` at the current position begins a new word (so a
74
+ * separator goes before it): true when the previous char is lower/digit, or when
75
+ * `C` ends an acronym run (two preceding uppercase chars) followed by a lowercase.
65
76
  */
66
- type CamelToSnake<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `_${Lowercase<C>}${CamelToSnake<R>}` : `${C}${CamelToSnake<R>}` : S;
77
+ type NeedsSeparator<C extends string, Prev extends string, PrevPrev extends string, Next extends string> = IsUpper<C> extends true ? IsLowerOrDigit<Prev> extends true ? true : IsUpper<Prev> extends true ? IsUpper<PrevPrev> extends true ? IsLower<Next> : false : false : false;
78
+ type SeparatorScan<S extends string, Sep extends string, Prev extends string = '', PrevPrev extends string = '', Acc extends string = ''> = S extends `${infer C}${infer R}` ? SeparatorScan<R, Sep, C, Prev, `${Acc}${NeedsSeparator<C, Prev, PrevPrev, FirstChar<R>> extends true ? Sep : ''}${Lowercase<C>}`> : Acc;
79
+ type SeparatorCase<S extends string, Sep extends string> = SeparatorScan<S, Sep>;
80
+ /**
81
+ * Type utility that transforms a camelCase string to snake_case (acronym-aware)
82
+ * Example: 'helloWorld' -> 'hello_world', 'getHTTPResponse' -> 'get_http_response'
83
+ */
84
+ type CamelToSnake<S extends string> = SeparatorCase<S, '_'>;
67
85
  /**
68
86
  * Type utility that transforms a camelCase string to PascalCase
69
87
  * Example: 'helloWorld' -> 'HelloWorld'
70
88
  */
71
89
  type CamelToPascal<S extends string> = Capitalize<S>;
72
90
  /**
73
- * Type utility that transforms a camelCase string to kebab-case
74
- * Example: 'helloWorld' -> 'hello-world'
91
+ * Type utility that transforms a camelCase string to kebab-case (acronym-aware)
92
+ * Example: 'helloWorld' -> 'hello-world', 'getHTTPResponse' -> 'get-http-response'
75
93
  */
76
- type CamelToKebab<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `-${Lowercase<C>}${CamelToKebab<R>}` : `${C}${CamelToKebab<R>}` : S;
94
+ type CamelToKebab<S extends string> = SeparatorCase<S, '-'>;
77
95
  /**
78
96
  * Type utility that transforms a PascalCase string to camelCase
79
97
  * Example: 'HelloWorld' -> 'helloWorld'
80
98
  */
81
99
  type PascalToCamel<S extends string> = S extends `${infer F}${infer R}` ? `${Lowercase<F>}${R}` : S;
82
100
  /**
83
- * Type utility that transforms a PascalCase string to snake_case
84
- * Example: 'HelloWorld' -> 'hello_world'
101
+ * Type utility that transforms a PascalCase string to snake_case (acronym-aware)
102
+ * Example: 'HelloWorld' -> 'hello_world', 'HTTPResponse' -> 'http_response'
85
103
  */
86
- type PascalToSnake<S extends string> = S extends `${infer F}${infer R}` ? R extends `` ? Lowercase<F> : `${Lowercase<F>}${PascalToSnakeInner<R>}` : S;
87
- type PascalToSnakeInner<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `_${Lowercase<C>}${PascalToSnakeInner<R>}` : `${C}${PascalToSnakeInner<R>}` : S;
104
+ type PascalToSnake<S extends string> = SeparatorCase<S, '_'>;
88
105
  /**
89
- * Type utility that transforms a PascalCase string to kebab-case
90
- * Example: 'HelloWorld' -> 'hello-world'
106
+ * Type utility that transforms a PascalCase string to kebab-case (acronym-aware)
107
+ * Example: 'HelloWorld' -> 'hello-world', 'HTTPResponse' -> 'http-response'
91
108
  */
92
- type PascalToKebab<S extends string> = S extends `${infer F}${infer R}` ? R extends `` ? Lowercase<F> : `${Lowercase<F>}${PascalToKebabInner<R>}` : S;
93
- type PascalToKebabInner<S extends string> = S extends `${infer C}${infer R}` ? C extends Uppercase<C> ? `-${Lowercase<C>}${PascalToKebabInner<R>}` : `${C}${PascalToKebabInner<R>}` : S;
109
+ type PascalToKebab<S extends string> = SeparatorCase<S, '-'>;
94
110
  /**
95
111
  * Type utility that transforms a kebab-case string to camelCase
96
112
  * Example: 'hello-world' -> 'helloWorld'
@@ -122,7 +138,20 @@ type KebabToSnake<S extends string> = S extends `${infer P}-${infer R}` ? `${P}_
122
138
  */
123
139
  type ForbiddenStartChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
124
140
  type ForbiddenEndChars = '_' | '-' | '$' | '&' | '%' | '#' | '@' | '!' | '*' | '+' | '=' | '~' | '`' | '^' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' | ':' | ';' | '"' | "'" | '<' | '>' | ',' | '.' | '?' | '/' | `${number}`;
125
- type PreservedKey<K extends string> = K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
141
+ /**
142
+ * The character set the runtime is willing to transform. Any key containing a
143
+ * character outside it — a symbol, or a non-ASCII letter like `é` — is preserved
144
+ * wholesale at runtime (`/[^A-Za-z0-9_-]/`), so the type must preserve it too.
145
+ * An explicit ASCII set is required because TS's `Uppercase`/`Lowercase` are
146
+ * Unicode-aware and would otherwise treat `é` as an ordinary transformable letter.
147
+ */
148
+ type AllowedKeyChar = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '_' | '-';
149
+ /**
150
+ * True if `S` contains any character outside `AllowedKeyChar`. Mirrors the runtime's
151
+ * `/[^A-Za-z0-9_-]/` preserve check.
152
+ */
153
+ type HasOutOfCharsetChar<S extends string> = S extends `${infer C}${infer R}` ? C extends AllowedKeyChar ? HasOutOfCharsetChar<R> : true : false;
154
+ type PreservedKey<K extends string> = HasOutOfCharsetChar<K> extends true ? true : K extends `${infer First}${string}` ? First extends ForbiddenStartChars ? true : K extends `${string}${infer Last}` ? Last extends ForbiddenEndChars ? true : K extends `${string}__${string}` | `${string}--${string}` ? true : false : false : false;
126
155
  /**
127
156
  * Type utility that selects the correct string transformation based on source and target casing
128
157
  */
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";const e={snake:{camel:function(e){return e.toLowerCase().replace(/_([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/_+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:function(e){return e.replace(/_/g,"-")},snake:e=>e},camel:{snake:function(e){if(!e)return e;let t=e.replace(/([a-z0-9])([A-Z])/g,"$1_$2");return t=t.replace(/([A-Z])(?=[A-Z])/g,"$1_"),t.toLowerCase()},pascal:function(e){return e?e.charAt(0).toUpperCase()+e.slice(1):e},kebab:function(e){if(!e)return e;let t=e.replace(/([a-z0-9])([A-Z])/g,"$1-$2");return t=t.replace(/([A-Z])(?=[A-Z])/g,"$1-"),t.toLowerCase()},camel:e=>e},pascal:{snake:function(e){if(!e)return e;let t=e.replace(/([A-Z]{2,})([A-Z][a-z])/g,"$1_$2");return t=t.replace(/([a-z0-9])([A-Z])/g,"$1_$2"),t.toLowerCase()},camel:function(e){return e?e.charAt(0).toLowerCase()+e.slice(1):e},kebab:function(e){if(!e)return e;let t=e.replace(/([A-Z]{2,})([A-Z][a-z])/g,"$1-$2");return t=t.replace(/([a-z0-9])([A-Z])/g,"$1-$2"),t.toLowerCase()},pascal:e=>e},kebab:{snake:function(e){return e.replace(/-/g,"_")},camel:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:e=>e}};const t=/^[a-z0-9_]+$/,r=/__+/,n=/^[a-z][a-zA-Z0-9]*$/,s=/^[A-Z][a-zA-Z0-9]*$/,a=/^[a-z0-9-]+$/,o=/-{2,}/,c=/[A-Z]/,u=/[a-z]/,i=/^[a-z0-9]+$/;function l(e){return"string"!=typeof e?null:function(e){if(e.includes("_")||e.includes("-")||e.includes(" "))return!1;if(e.length>0&&!/^[A-Za-z0-9]+$/.test(e))return!1;if(e.length<=1)return!0;const t=e[0],r=e.slice(1);return e===e.toLowerCase()||t===t.toUpperCase()&&r===r.toLowerCase()}(e)?"single":function(e){return"string"==typeof e&&t.test(e)&&!r.test(e)&&(e.includes("_")||i.test(e))}(e)?"snake":function(e){return"string"==typeof e&&n.test(e)&&c.test(e)}(e)?"camel":function(e){return"string"==typeof e&&s.test(e)&&u.test(e)}(e)?"pascal":function(e){return"string"==typeof e&&a.test(e)&&!o.test(e)&&(e.includes("-")||i.test(e))}(e)?"kebab":null}function f(t,r){if(!t)return t;const{targetCase:n}=r,{string:s,shouldTransform:a,sourceCase:o,singleWord:c}=function(e){if("string"!=typeof e)throw new Error("Input must be a string");if(0===e.length)return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};if(/[^A-Za-z0-9_-]/.test(e)||/^[^A-Za-z]/.test(e)||/[^A-Za-z0-9]$/.test(e))return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};const t=l(e);return t?"single"===t?{string:e,shouldTransform:!0,sourceCase:null,singleWord:!0}:{string:e,shouldTransform:!0,sourceCase:t,singleWord:!1}:{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1}}(t);if(!a)return s;if(c)return function(e,t){switch(t){case"pascal":return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase();case"camel":return e.charAt(0).toLowerCase()+e.slice(1);case"snake":case"kebab":return e.toLowerCase();default:return e}}(s,n);if(!o||o===n)return s;const u=e[o];if(!u)return s;const i=u[n];return i?i(s):s}function p(e,t){if(!(e instanceof Map))return e;const r=[];for(const[n,s]of e.entries())if("string"==typeof n){const e=f(n,t);r.push([e,s])}else r.push([n,s]);return new Map(r)}function g(e,t){if(null===e||"object"!=typeof e||Array.isArray(e))return e;const{targetCase:r,sourceCase:n}=t,s={};for(const t in e)if(Object.prototype.hasOwnProperty.call(e,t)){s[!isNaN(Number(t))&&String(Number(t))===t?Number(t):f(t,{targetCase:r})]=e[t]}for(const t of Object.getOwnPropertySymbols(e))s[t]=e[t];return s}function C(e,t,r=new WeakMap,n=0){var s;if(null==e||"object"!=typeof e)return e;if(r.has(e))return r.get(e);const a=null!==(s=t.depth)&&void 0!==s?s:1/0;if(n>=a)return e;const o=Object.assign(Object.assign({},t),{depth:a});let c;if(Array.isArray(e))c=function(e,t){return Array.isArray(e)?e.map(e=>null==e?e:e instanceof Map?p(e,t):"object"!=typeof e||Array.isArray(e)||e instanceof Set?e:g(e,t)):e}(e,t),r.set(e,c),c=c.map(e=>C(e,o,r,n+1));else if(e instanceof Map){c=p(e,t),r.set(e,c);const s=c;for(const[e,t]of s.entries())null!==t&&"object"==typeof t&&s.set(e,C(t,o,r,n+1))}else if(e instanceof Set){c=new Set(e),r.set(e,c);const t=c,s=Array.from(t);t.clear();for(const e of s)null!==e&&"object"==typeof e?t.add(C(e,o,r,n+1)):t.add(e)}else{c=g(e,t),r.set(e,c);const s=c;for(const e in s){const t=s[e];null!==t&&"object"==typeof t&&(s[e]=C(t,o,r,n+1))}}return c}exports.transform=function(e,t){var r;return C(e,Object.assign(Object.assign({},t),{targetCase:(null==t?void 0:t.targetCase)||"camel",depth:null!==(r=null==t?void 0:t.depth)&&void 0!==r?r:1/0}))};
1
+ "use strict";const e=e=>e>="A"&&e<="Z",t=e=>e>="a"&&e<="z",n=e=>e>="0"&&e<="9";function r(r,s){var o,a,c;if(!r)return r;let u="";for(let i=0;i<r.length;i++){const l=r[i];if(e(l)){const l=null!==(o=r[i-1])&&void 0!==o?o:"",f=t(l)||n(l),p=e(l)&&e(null!==(a=r[i-2])&&void 0!==a?a:"")&&t(null!==(c=r[i+1])&&void 0!==c?c:"");(f||p)&&(u+=s)}u+=l.toLowerCase()}return u}const s={snake:{camel:function(e){return e.toLowerCase().replace(/_([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/_+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:function(e){return e.replace(/_/g,"-")},snake:e=>e},camel:{snake:function(e){return r(e,"_")},pascal:function(e){return e?e.charAt(0).toUpperCase()+e.slice(1):e},kebab:function(e){return r(e,"-")},camel:e=>e},pascal:{snake:function(e){return r(e,"_")},camel:function(e){return e?e.charAt(0).toLowerCase()+e.slice(1):e},kebab:function(e){return r(e,"-")},pascal:e=>e},kebab:{snake:function(e){return e.replace(/-/g,"_")},camel:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:e=>e}};const o=/^[a-z0-9_]+$/,a=/__+/,c=/^[a-z][a-zA-Z0-9]*$/,u=/^[A-Z][a-zA-Z0-9]*$/,i=/^[a-z0-9-]+$/,l=/-{2,}/,f=/[A-Z]/,p=/[a-z]/,g=/^[a-z0-9]+$/;function d(e){return"string"!=typeof e?null:function(e){if(e.includes("_")||e.includes("-")||e.includes(" "))return!1;if(e.length>0&&!/^[A-Za-z0-9]+$/.test(e))return!1;if(e.length<=1)return!0;const t=e[0],n=e.slice(1);return e===e.toLowerCase()||t===t.toUpperCase()&&n===n.toLowerCase()}(e)?"single":function(e){return"string"==typeof e&&o.test(e)&&!a.test(e)&&(e.includes("_")||g.test(e))}(e)?"snake":function(e){return"string"==typeof e&&c.test(e)&&f.test(e)}(e)?"camel":function(e){return"string"==typeof e&&u.test(e)&&p.test(e)}(e)?"pascal":function(e){return"string"==typeof e&&i.test(e)&&!l.test(e)&&(e.includes("-")||g.test(e))}(e)?"kebab":null}function b(e,t){if(!e)return e;const{targetCase:n}=t,{string:r,shouldTransform:o,sourceCase:a,singleWord:c}=function(e){if("string"!=typeof e)throw new Error("Input must be a string");if(0===e.length)return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};if(/[^A-Za-z0-9_-]/.test(e)||/^[^A-Za-z]/.test(e)||/[^A-Za-z0-9]$/.test(e))return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};const t=d(e);return t?"single"===t?{string:e,shouldTransform:!0,sourceCase:null,singleWord:!0}:{string:e,shouldTransform:!0,sourceCase:t,singleWord:!1}:{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1}}(e);if(!o)return r;if(c)return function(e,t){switch(t){case"pascal":return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase();case"camel":return e.charAt(0).toLowerCase()+e.slice(1);case"snake":case"kebab":return e.toLowerCase();default:return e}}(r,n);if(!a||a===n)return r;const u=s[a];if(!u)return r;const i=u[n];return i?i(r):r}function C(e,t,n){"__proto__"===t?Object.defineProperty(e,t,{value:n,enumerable:!0,writable:!0,configurable:!0}):e[t]=n}function h(e,t,n=new WeakMap,r=0){var s;if(null==e||"object"!=typeof e)return e;if(n.has(e))return n.get(e);const o=null!==(s=t.depth)&&void 0!==s?s:1/0;if(r>=o)return e;const a=t.depth===o?t:Object.assign(Object.assign({},t),{depth:o});let c;if(Array.isArray(e))c=function(e,t,n){const r=[];t(r);for(const t of e)r.push(n(t));return r}(e,t=>n.set(e,t),e=>h(e,a,n,r));else if(e instanceof Map){c=function(e,t){if(!(e instanceof Map))return e;const n=[];for(const[r,s]of e.entries())if("string"==typeof r){const e=b(r,t);n.push([e,s])}else n.push([r,s]);return new Map(n)}(e,t),n.set(e,c);const s=c;for(const[e,t]of s.entries())null!==t&&"object"==typeof t&&s.set(e,h(t,a,n,r+1))}else if(e instanceof Set){c=new Set(e),n.set(e,c);const t=c,s=Array.from(t);t.clear();for(const e of s)null!==e&&"object"==typeof e?t.add(h(e,a,n,r+1)):t.add(e)}else{c=function(e,t){if(null===e||"object"!=typeof e||Array.isArray(e))return e;const{targetCase:n,sourceCase:r}=t,s={};for(const t in e)Object.prototype.hasOwnProperty.call(e,t)&&C(s,isNaN(Number(t))||String(Number(t))!==t?b(t,{targetCase:n}):Number(t),e[t]);for(const t of Object.getOwnPropertySymbols(e))s[t]=e[t];return s}(e,t),n.set(e,c);const s=c;for(const e in s){const t=s[e];null!==t&&"object"==typeof t&&C(s,e,h(t,a,n,r+1))}}return c}exports.transform=function(e,t){var n;return h(e,Object.assign(Object.assign({},t),{targetCase:(null==t?void 0:t.targetCase)||"camel",depth:null!==(n=null==t?void 0:t.depth)&&void 0!==n?n:1/0}))};
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- const e={snake:{camel:function(e){return e.toLowerCase().replace(/_([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/_+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:function(e){return e.replace(/_/g,"-")},snake:e=>e},camel:{snake:function(e){if(!e)return e;let t=e.replace(/([a-z0-9])([A-Z])/g,"$1_$2");return t=t.replace(/([A-Z])(?=[A-Z])/g,"$1_"),t.toLowerCase()},pascal:function(e){return e?e.charAt(0).toUpperCase()+e.slice(1):e},kebab:function(e){if(!e)return e;let t=e.replace(/([a-z0-9])([A-Z])/g,"$1-$2");return t=t.replace(/([A-Z])(?=[A-Z])/g,"$1-"),t.toLowerCase()},camel:e=>e},pascal:{snake:function(e){if(!e)return e;let t=e.replace(/([A-Z]{2,})([A-Z][a-z])/g,"$1_$2");return t=t.replace(/([a-z0-9])([A-Z])/g,"$1_$2"),t.toLowerCase()},camel:function(e){return e?e.charAt(0).toLowerCase()+e.slice(1):e},kebab:function(e){if(!e)return e;let t=e.replace(/([A-Z]{2,})([A-Z][a-z])/g,"$1-$2");return t=t.replace(/([a-z0-9])([A-Z])/g,"$1-$2"),t.toLowerCase()},pascal:e=>e},kebab:{snake:function(e){return e.replace(/-/g,"_")},camel:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:e=>e}};const t=/^[a-z0-9_]+$/,r=/__+/,n=/^[a-z][a-zA-Z0-9]*$/,s=/^[A-Z][a-zA-Z0-9]*$/,a=/^[a-z0-9-]+$/,o=/-{2,}/,c=/[A-Z]/,u=/[a-z]/,i=/^[a-z0-9]+$/;function l(e){return"string"!=typeof e?null:function(e){if(e.includes("_")||e.includes("-")||e.includes(" "))return!1;if(e.length>0&&!/^[A-Za-z0-9]+$/.test(e))return!1;if(e.length<=1)return!0;const t=e[0],r=e.slice(1);return e===e.toLowerCase()||t===t.toUpperCase()&&r===r.toLowerCase()}(e)?"single":function(e){return"string"==typeof e&&t.test(e)&&!r.test(e)&&(e.includes("_")||i.test(e))}(e)?"snake":function(e){return"string"==typeof e&&n.test(e)&&c.test(e)}(e)?"camel":function(e){return"string"==typeof e&&s.test(e)&&u.test(e)}(e)?"pascal":function(e){return"string"==typeof e&&a.test(e)&&!o.test(e)&&(e.includes("-")||i.test(e))}(e)?"kebab":null}function f(t,r){if(!t)return t;const{targetCase:n}=r,{string:s,shouldTransform:a,sourceCase:o,singleWord:c}=function(e){if("string"!=typeof e)throw new Error("Input must be a string");if(0===e.length)return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};if(/[^A-Za-z0-9_-]/.test(e)||/^[^A-Za-z]/.test(e)||/[^A-Za-z0-9]$/.test(e))return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};const t=l(e);return t?"single"===t?{string:e,shouldTransform:!0,sourceCase:null,singleWord:!0}:{string:e,shouldTransform:!0,sourceCase:t,singleWord:!1}:{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1}}(t);if(!a)return s;if(c)return function(e,t){switch(t){case"pascal":return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase();case"camel":return e.charAt(0).toLowerCase()+e.slice(1);case"snake":case"kebab":return e.toLowerCase();default:return e}}(s,n);if(!o||o===n)return s;const u=e[o];if(!u)return s;const i=u[n];return i?i(s):s}function p(e,t){if(!(e instanceof Map))return e;const r=[];for(const[n,s]of e.entries())if("string"==typeof n){const e=f(n,t);r.push([e,s])}else r.push([n,s]);return new Map(r)}function g(e,t){if(null===e||"object"!=typeof e||Array.isArray(e))return e;const{targetCase:r,sourceCase:n}=t,s={};for(const t in e)if(Object.prototype.hasOwnProperty.call(e,t)){s[!isNaN(Number(t))&&String(Number(t))===t?Number(t):f(t,{targetCase:r})]=e[t]}for(const t of Object.getOwnPropertySymbols(e))s[t]=e[t];return s}function C(e,t,r=new WeakMap,n=0){var s;if(null==e||"object"!=typeof e)return e;if(r.has(e))return r.get(e);const a=null!==(s=t.depth)&&void 0!==s?s:1/0;if(n>=a)return e;const o=Object.assign(Object.assign({},t),{depth:a});let c;if(Array.isArray(e))c=function(e,t){return Array.isArray(e)?e.map(e=>null==e?e:e instanceof Map?p(e,t):"object"!=typeof e||Array.isArray(e)||e instanceof Set?e:g(e,t)):e}(e,t),r.set(e,c),c=c.map(e=>C(e,o,r,n+1));else if(e instanceof Map){c=p(e,t),r.set(e,c);const s=c;for(const[e,t]of s.entries())null!==t&&"object"==typeof t&&s.set(e,C(t,o,r,n+1))}else if(e instanceof Set){c=new Set(e),r.set(e,c);const t=c,s=Array.from(t);t.clear();for(const e of s)null!==e&&"object"==typeof e?t.add(C(e,o,r,n+1)):t.add(e)}else{c=g(e,t),r.set(e,c);const s=c;for(const e in s){const t=s[e];null!==t&&"object"==typeof t&&(s[e]=C(t,o,r,n+1))}}return c}function A(e,t){var r;return C(e,Object.assign(Object.assign({},t),{targetCase:(null==t?void 0:t.targetCase)||"camel",depth:null!==(r=null==t?void 0:t.depth)&&void 0!==r?r:1/0}))}export{A as transform};
1
+ const e=e=>e>="A"&&e<="Z",t=e=>e>="a"&&e<="z",n=e=>e>="0"&&e<="9";function r(r,s){var o,a,c;if(!r)return r;let u="";for(let i=0;i<r.length;i++){const l=r[i];if(e(l)){const l=null!==(o=r[i-1])&&void 0!==o?o:"",f=t(l)||n(l),p=e(l)&&e(null!==(a=r[i-2])&&void 0!==a?a:"")&&t(null!==(c=r[i+1])&&void 0!==c?c:"");(f||p)&&(u+=s)}u+=l.toLowerCase()}return u}const s={snake:{camel:function(e){return e.toLowerCase().replace(/_([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/_+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:function(e){return e.replace(/_/g,"-")},snake:e=>e},camel:{snake:function(e){return r(e,"_")},pascal:function(e){return e?e.charAt(0).toUpperCase()+e.slice(1):e},kebab:function(e){return r(e,"-")},camel:e=>e},pascal:{snake:function(e){return r(e,"_")},camel:function(e){return e?e.charAt(0).toLowerCase()+e.slice(1):e},kebab:function(e){return r(e,"-")},pascal:e=>e},kebab:{snake:function(e){return e.replace(/-/g,"_")},camel:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase())},pascal:function(e){return e.toLowerCase().replace(/-+([a-z0-9])/g,(e,t)=>t.toUpperCase()).replace(/^[a-z]/,e=>e.toUpperCase())},kebab:e=>e}};const o=/^[a-z0-9_]+$/,a=/__+/,c=/^[a-z][a-zA-Z0-9]*$/,u=/^[A-Z][a-zA-Z0-9]*$/,i=/^[a-z0-9-]+$/,l=/-{2,}/,f=/[A-Z]/,p=/[a-z]/,g=/^[a-z0-9]+$/;function d(e){return"string"!=typeof e?null:function(e){if(e.includes("_")||e.includes("-")||e.includes(" "))return!1;if(e.length>0&&!/^[A-Za-z0-9]+$/.test(e))return!1;if(e.length<=1)return!0;const t=e[0],n=e.slice(1);return e===e.toLowerCase()||t===t.toUpperCase()&&n===n.toLowerCase()}(e)?"single":function(e){return"string"==typeof e&&o.test(e)&&!a.test(e)&&(e.includes("_")||g.test(e))}(e)?"snake":function(e){return"string"==typeof e&&c.test(e)&&f.test(e)}(e)?"camel":function(e){return"string"==typeof e&&u.test(e)&&p.test(e)}(e)?"pascal":function(e){return"string"==typeof e&&i.test(e)&&!l.test(e)&&(e.includes("-")||g.test(e))}(e)?"kebab":null}function b(e,t){if(!e)return e;const{targetCase:n}=t,{string:r,shouldTransform:o,sourceCase:a,singleWord:c}=function(e){if("string"!=typeof e)throw new Error("Input must be a string");if(0===e.length)return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};if(/[^A-Za-z0-9_-]/.test(e)||/^[^A-Za-z]/.test(e)||/[^A-Za-z0-9]$/.test(e))return{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1};const t=d(e);return t?"single"===t?{string:e,shouldTransform:!0,sourceCase:null,singleWord:!0}:{string:e,shouldTransform:!0,sourceCase:t,singleWord:!1}:{string:e,shouldTransform:!1,sourceCase:null,singleWord:!1}}(e);if(!o)return r;if(c)return function(e,t){switch(t){case"pascal":return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase();case"camel":return e.charAt(0).toLowerCase()+e.slice(1);case"snake":case"kebab":return e.toLowerCase();default:return e}}(r,n);if(!a||a===n)return r;const u=s[a];if(!u)return r;const i=u[n];return i?i(r):r}function C(e,t,n){"__proto__"===t?Object.defineProperty(e,t,{value:n,enumerable:!0,writable:!0,configurable:!0}):e[t]=n}function h(e,t,n=new WeakMap,r=0){var s;if(null==e||"object"!=typeof e)return e;if(n.has(e))return n.get(e);const o=null!==(s=t.depth)&&void 0!==s?s:1/0;if(r>=o)return e;const a=t.depth===o?t:Object.assign(Object.assign({},t),{depth:o});let c;if(Array.isArray(e))c=function(e,t,n){const r=[];t(r);for(const t of e)r.push(n(t));return r}(e,t=>n.set(e,t),e=>h(e,a,n,r));else if(e instanceof Map){c=function(e,t){if(!(e instanceof Map))return e;const n=[];for(const[r,s]of e.entries())if("string"==typeof r){const e=b(r,t);n.push([e,s])}else n.push([r,s]);return new Map(n)}(e,t),n.set(e,c);const s=c;for(const[e,t]of s.entries())null!==t&&"object"==typeof t&&s.set(e,h(t,a,n,r+1))}else if(e instanceof Set){c=new Set(e),n.set(e,c);const t=c,s=Array.from(t);t.clear();for(const e of s)null!==e&&"object"==typeof e?t.add(h(e,a,n,r+1)):t.add(e)}else{c=function(e,t){if(null===e||"object"!=typeof e||Array.isArray(e))return e;const{targetCase:n,sourceCase:r}=t,s={};for(const t in e)Object.prototype.hasOwnProperty.call(e,t)&&C(s,isNaN(Number(t))||String(Number(t))!==t?b(t,{targetCase:n}):Number(t),e[t]);for(const t of Object.getOwnPropertySymbols(e))s[t]=e[t];return s}(e,t),n.set(e,c);const s=c;for(const e in s){const t=s[e];null!==t&&"object"==typeof t&&C(s,e,h(t,a,n,r+1))}}return c}function y(e,t){var n;return h(e,Object.assign(Object.assign({},t),{targetCase:(null==t?void 0:t.targetCase)||"camel",depth:null!==(n=null==t?void 0:t.depth)&&void 0!==n?n:1/0}))}export{y as transform};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deep-case-crafter",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "Transforms deeply nested object, array, Map, and Set keys between common case formats while preserving TypeScript type safety",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -60,10 +60,12 @@
60
60
  "prepare": "husky",
61
61
  "test": "jest",
62
62
  "test-coverage": "npm test -- --coverage",
63
+ "test:types": "tsc --noEmit",
63
64
  "tsc": "tsc --noEmit",
64
65
  "lint": "eslint --ext .js,.jsx,.ts,.tsx src/ --fix --max-warnings=0 --no-cache",
65
66
  "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\"",
66
- "benchmark": "tsx benchmark/*.benchmark.ts"
67
+ "benchmark": "tsx benchmark/*.benchmark.ts",
68
+ "check": "npm run lint && npm run test:types && npm run test-coverage && npm run build"
67
69
  },
68
70
  "devDependencies": {
69
71
  "@eslint/js": "10.0.1",
package/readme.md CHANGED
@@ -13,8 +13,9 @@
13
13
  ## Features
14
14
 
15
15
  - Converts keys to `camelCase`, `PascalCase`, `snake_case`, and `kebab-case`.
16
+ - **Acronym-aware**: `HTTPResponse` → `http_response`, `userID` → `user_id` — not `h_t_t_p_...`.
16
17
  - Works with deeply nested structures, including objects, arrays, Maps, and Sets.
17
- - Precise TypeScript inference when `sourceCase` and `targetCase` are set (otherwise the result type is widened).
18
+ - Precise TypeScript inference when `sourceCase` and `targetCase` are set (otherwise the result is `unknown`).
18
19
  - Preserves special keys (`__`, `--`, leading numbers, special characters).
19
20
  - Configurable recursion depth (default: fully recursive).
20
21
 
@@ -145,6 +146,25 @@ console.log(transform(strings, { targetCase: 'camel', sourceCase: 'snake' }));
145
146
 
146
147
  ---
147
148
 
149
+ ## Acronym-aware conversion
150
+
151
+ When converting **to** `snake_case` or `kebab-case`, runs of capitals (acronyms)
152
+ are treated as whole words instead of being split letter-by-letter:
153
+
154
+ ```typescript
155
+ const input = { getHTTPResponse: 1, userID: 2, parseHTMLString: 3 };
156
+ const result = transform(input, { sourceCase: 'camel', targetCase: 'snake' });
157
+
158
+ console.log(result);
159
+ // Output: { get_http_response: 1, user_id: 2, parse_html_string: 3 }
160
+ // (not { get_h_t_t_p_response, user_i_d, ... })
161
+ ```
162
+
163
+ The output **types** match this exactly when `sourceCase` and `targetCase` are
164
+ set, so `result.get_http_response` is known at compile time.
165
+
166
+ ---
167
+
148
168
  ## API
149
169
 
150
170
  ### `transform(data, options)`
@@ -157,6 +177,14 @@ console.log(transform(strings, { targetCase: 'camel', sourceCase: 'snake' }));
157
177
 
158
178
  > **TypeScript inference:** precise output types are produced only when both `sourceCase` and `targetCase` are passed. Without both, the call still works at runtime (auto-detecting each key) but the result type is `unknown`.
159
179
 
180
+ > **`sourceCase` is a type-level assertion, not a runtime switch.** At runtime every
181
+ > key's case is auto-detected regardless of `sourceCase`, so the precise types are
182
+ > accurate only for keys genuinely well-formed in the declared source case. Ambiguous
183
+ > keys — mixed case (`mixed_Case`), `SCREAMING_SNAKE`, or mixed separators (`a_b-c`) — are
184
+ > **preserved unchanged at runtime**, even though the type may show them transformed. Set
185
+ > `sourceCase` only when your keys really are that case. (Out-of-charset keys such as
186
+ > `café_table` or `a.b_c` are preserved at both layers.)
187
+
160
188
  ---
161
189
 
162
190
  ## Advanced: explicit output type overload