Skip to content

Commit

Permalink
Fix: Simple Pseudo Selectors as typesafe (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
black7375 authored Jan 10, 2024
1 parent 5300482 commit 6faf887
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { NonNullableString } from "../types/string";
import { SimplePseudos } from "../types/simple-pseudo";
import { SimplePseudos, CamelPseudos } from "../types/simple-pseudo";

// == Type ================================================================
type PseudoSelectorsSign = `_${string}` | `__${string}`;
type InputKeyValue = NonNullableString | PseudoSelectorsSign;
type InputKeyValue = NonNullableString | CamelPseudos;
type ReturnKeyValue = NonNullableString | SimplePseudos;

// == Interface ================================================================
export function replacePseudoSelectors(keyStr: CamelPseudos): SimplePseudos;
export function replacePseudoSelectors(
keyStr: NonNullableString
): NonNullableString;
export function replacePseudoSelectors(keyStr: InputKeyValue): ReturnKeyValue {
const kebabKeyStr = keyStr.startsWith("_") ? camelToKebab(keyStr) : keyStr;

Expand All @@ -18,8 +22,9 @@ export function replacePseudoSelectors(keyStr: InputKeyValue): ReturnKeyValue {
}

// == Utils ====================================================================
const upperCaseRegex = /[A-Z]/g;
function camelToKebab(camelCase: InputKeyValue) {
return camelCase.replace(/[A-Z]/g, "-$&").toLowerCase();
return camelCase.replace(upperCaseRegex, "-$&").toLowerCase();
}
function hasSinglePseudoSelector(
value: InputKeyValue
Expand All @@ -38,36 +43,50 @@ if (import.meta.vitest) {

describe.concurrent("Replace Simple Pseudo Selectors Sign", () => {
it("No Simple Pseudo", () => {
expect(replacePseudoSelectors("after")).toBe("after");
expect(replacePseudoSelectors("backgroundColor")).toBe("backgroundColor");
expect(replacePseudoSelectors(".myClassName > &")).toBe(
".myClassName > &"
);
expect(replacePseudoSelectors("&:hover:not(:active)")).toBe(
const after = replacePseudoSelectors("after");
expect(after).toBe("after");
expectTypeOf(after).toEqualTypeOf<NonNullableString>();

const backgroundColor = replacePseudoSelectors("backgroundColor");
expect(backgroundColor).toBe("backgroundColor");
expectTypeOf(backgroundColor).toEqualTypeOf<NonNullableString>();

const complexSelector = replacePseudoSelectors(".myClass-Name > &");
expect(complexSelector).toBe(".myClass-Name > &");
expectTypeOf(complexSelector).toEqualTypeOf<NonNullableString>();

const otherComplexSelector = replacePseudoSelectors(
"&:hover:not(:active)"
);
expect(replacePseudoSelectors("@supports (display: grid)")).toBe(
"@supports (display: grid)"
);
expect(otherComplexSelector).toBe("&:hover:not(:active)");
expectTypeOf(otherComplexSelector).toEqualTypeOf<NonNullableString>();

const atRules = replacePseudoSelectors("@supports (display: grid)");
expect(atRules).toBe("@supports (display: grid)");
expectTypeOf(atRules).toEqualTypeOf<NonNullableString>();
});
it("Has Single or Double Simple Pseudo at the first", () => {
const singleSimplePseudo = replacePseudoSelectors("_hover");
expect(singleSimplePseudo).toBe(":hover");
expectTypeOf(singleSimplePseudo).toEqualTypeOf<ReturnKeyValue>();
expectTypeOf(singleSimplePseudo).toEqualTypeOf<SimplePseudos>();

const doubleSimplePseudo = replacePseudoSelectors("__before");
expect(doubleSimplePseudo).toBe("::before");
expectTypeOf(doubleSimplePseudo).toEqualTypeOf<ReturnKeyValue>();
expectTypeOf(doubleSimplePseudo).toEqualTypeOf<SimplePseudos>();

const simplePseudoWithCamel = replacePseudoSelectors("_firstOfType");
expect(simplePseudoWithCamel).toBe(":first-of-type");
expectTypeOf(simplePseudoWithCamel).toEqualTypeOf<SimplePseudos>();

const doubleSimplePseudoWithCamel =
replacePseudoSelectors("__firstOfType");
expect(doubleSimplePseudoWithCamel).toBe("::first-of-type");
expectTypeOf(doubleSimplePseudoWithCamel).toEqualTypeOf<ReturnKeyValue>();
replacePseudoSelectors("__firstLetter");
expect(doubleSimplePseudoWithCamel).toBe("::first-letter");
expectTypeOf(doubleSimplePseudoWithCamel).toEqualTypeOf<SimplePseudos>();
});
it("Has Triple Simple Pseudo at the first", () => {
const tripleSimplePseudo = replacePseudoSelectors("___active");
expect(tripleSimplePseudo).toBe("::_active");
expectTypeOf(tripleSimplePseudo).toEqualTypeOf<ReturnKeyValue>();
expectTypeOf(tripleSimplePseudo).toEqualTypeOf<NonNullableString>();
});
it("Has Simple Pseudo in the middle or at the end", () => {
expect(replacePseudoSelectors("hover_")).toBe("hover_");
Expand Down
11 changes: 9 additions & 2 deletions packages/transform-to-vanilla/src/types/simple-pseudo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { FromKebabCase, ColonToSnake } from "./string";

// == Interface ================================================================
export type SimplePseudos = keyof typeof simplePseudoMap;
export type CamelPseudos = CamelPseudo<SimplePseudos>;

// == Utils ====================================================================
type CamelPseudo<T extends string> = ColonToSnake<FromKebabCase<T>>;

// This types are refer to vanilla-extract/css
declare const simplePseudoMap: {
readonly ":-moz-any-link": true;
Expand Down Expand Up @@ -96,5 +105,3 @@ declare const simplePseudoMap: {
readonly ":valid": true;
readonly ":visited": true;
};

export type SimplePseudos = keyof typeof simplePseudoMap;
54 changes: 54 additions & 0 deletions packages/transform-to-vanilla/src/types/string.ts
Original file line number Diff line number Diff line change
@@ -1 +1,55 @@
// == Interface ================================================================
export type NonNullableString = string & NonNullable<unknown>;

export type ToKebabCase<
InputString extends string,
AccumulatorString extends string = ""
> = InputString extends `${infer FirstChar}${infer RemainingString}`
? ToKebabCase<
RemainingString,
`${AccumulatorString}${FirstChar extends Lowercase<FirstChar>
? ""
: "-"}${Lowercase<FirstChar>}`
>
: AccumulatorString;

export type FromKebabCase<InputString extends string> =
InputString extends `${infer BeforeString}-${infer AfterString}`
? `${BeforeString}${Capitalize<FromKebabCase<AfterString>>}`
: InputString;

export type ColonToSnake<InputString extends string> =
InputString extends `${infer BeforeString}:${infer AfterString}`
? `${BeforeString}_${ColonToSnake<AfterString>}`
: InputString;

// == Tests ====================================================================
if (import.meta.vitest) {
const { describe, it, expectTypeOf } = import.meta.vitest;

describe.concurrent("String type utils", () => {
it("Convert to kebab", () => {
type CamelCase = ToKebabCase<"backgroundColor">;
expectTypeOf<CamelCase>().toEqualTypeOf<"background-color">();

type PascalCase = ToKebabCase<"WebkitTapHighlightColor">;
expectTypeOf<PascalCase>().toEqualTypeOf<"-webkit-tap-highlight-color">();
});

it("Convert from kebab", () => {
type CamelCase = FromKebabCase<"background-color">;
expectTypeOf<CamelCase>().toEqualTypeOf<"backgroundColor">();

type PascalCase = FromKebabCase<"-webkit-tap-highlight-color">;
expectTypeOf<PascalCase>().toEqualTypeOf<"WebkitTapHighlightColor">();
});

it("Convert colon to snake", () => {
type SimplePseudo = ColonToSnake<":hover">;
expectTypeOf<SimplePseudo>().toEqualTypeOf<"_hover">();

type DoubleSimplePseudo = ColonToSnake<"::before">;
expectTypeOf<DoubleSimplePseudo>().toEqualTypeOf<"__before">();
});
});
}

0 comments on commit 6faf887

Please sign in to comment.