Skip to content
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
119 changes: 119 additions & 0 deletions src/KebabCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Equal } from "./typings/Equal";
import { IsTuple } from "./typings/IsTuple";
import { NativeClass } from "./typings/NativeClass";
import { ValueOf } from "./typings/ValueOf";

/**
* Kebab case type.
*
* `KebabCase` type is a type that all keys of an object are converted to kebab case.
*
* It also erases every method property like {@link Resolved} type.
*
* @template T Target type to be kebab cased
* @author Jeongho Nam - https://github.com/samchon
*/
export type KebabCase<T> =
Equal<T, KebabizeMain<T>> extends true ? T : KebabizeMain<T>;

/* -----------------------------------------------------------
OBJECT CONVERSION
----------------------------------------------------------- */

type KebabizeMain<T> = T extends [never]
? never // special trick for (jsonable | null) type
: T extends { valueOf(): boolean | bigint | number | string }
? ValueOf<T>
: T extends Function
? never
: T extends object
? KebabizeObject<T>
: T;

type KebabizeObject<T extends object> =
T extends Array<infer U>
? IsTuple<T> extends true
? KebabizeTuple<T>
: KebabizeMain<U>[]
: T extends Set<infer U>
? Set<KebabizeMain<U>>
: T extends Map<infer K, infer V>
? Map<KebabizeMain<K>, KebabizeMain<V>>
: T extends WeakSet<any> | WeakMap<any, any>
? never
: T extends NativeClass
? T
: {
[Key in keyof T as KebabizeString<Key & string>]: KebabizeMain<
T[Key]
>;
};

/* -----------------------------------------------------------
SPECIAL CASES
----------------------------------------------------------- */
type KebabizeTuple<T extends readonly any[]> = T extends []
? []
: T extends [infer F]
? [KebabizeMain<F>]
: T extends [infer F, ...infer Rest extends readonly any[]]
? [KebabizeMain<F>, ...KebabizeTuple<Rest>]
: T extends [(infer F)?]
? [KebabizeMain<F>?]
: T extends [(infer F)?, ...infer Rest extends readonly any[]]
? [KebabizeMain<F>?, ...KebabizeTuple<Rest>]
: [];

/* -----------------------------------------------------------
STRING CONVERTER
----------------------------------------------------------- */
type KebabizeString<Key extends string> = Key extends `${infer _}`
? KebabizeStringRepeatedly<Key, "">
: Key;
type KebabizeStringRepeatedly<
S extends string,
Previous extends string,
> = S extends `${infer First}${infer Second}${infer Rest}`
? `${Hyphen<Previous, First>}${Lowercase<First>}${Hyphen<
First,
Second
>}${Lowercase<Second>}${KebabizeStringRepeatedly<Rest, Second>}`
: S extends `${infer First}`
? `${Hyphen<Previous, First>}${Lowercase<First>}`
: "";
type Hyphen<First extends string, Second extends string> = First extends
| UpperAlphabetic
| ""
| "-"
| "_"
? ""
: Second extends UpperAlphabetic
? "-"
: "";
type UpperAlphabetic =
| "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";
52 changes: 52 additions & 0 deletions src/internal/_notationKebab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { __notationUnsnake } from "./private/__notationUnsnake";

export const _notationKebab = (str: string): string => {
if (str.length === 0) return str;

// PREFIX (handle leading hyphens and underscores)
let prefix: string = "";
for (let i: number = 0; i < str.length; i++) {
if (str[i] === "-" || str[i] === "_") prefix += str[i];
else break;
}
if (prefix.length !== 0) str = str.substring(prefix.length);

const out = (s: string) => `${prefix}${s}`;

// Handle snake_case input
if (str.includes("_")) {
const items: string[] = str.split("_");
return out(items.map((s) => s.toLowerCase()).join("-"));
}

// Handle kebab-case input (already kebab)
if (str.includes("-")) {
const items: string[] = str.split("-");
return out(items.map((s) => s.toLowerCase()).join("-"));
}

// CAMEL OR PASCAL CASE
const indexes: number[] = [];
for (let i: number = 0; i < str.length; i++) {
const code: number = str.charCodeAt(i);
if (65 <= code && code <= 90) indexes.push(i);
}
for (let i: number = indexes.length - 1; i > 0; --i) {
const now: number = indexes[i]!;
const prev: number = indexes[i - 1]!;
if (now - prev === 1) indexes.splice(i, 1);
}
if (indexes.length !== 0 && indexes[0] === 0) indexes.splice(0, 1);
if (indexes.length === 0) return out(str.toLowerCase());

let ret: string = "";
for (let i: number = 0; i < indexes.length; i++) {
const first: number = i === 0 ? 0 : indexes[i - 1]!;
const last: number = indexes[i]!;

ret += str.substring(first, last).toLowerCase();
ret += "-";
}
ret += str.substring(indexes[indexes.length - 1]!).toLowerCase();
return out(ret);
}
1 change: 1 addition & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export * from "./TypeGuardError";
export * from "./Primitive";
export * from "./Resolved";
export * from "./CamelCase";
export * from "./KebabCase";
export * from "./PascalCase";
export * from "./SnakeCase";
export * from "./IReadableURLSearchParams";
Expand Down
Loading
Loading