from typing import TypedDict, Union, Literal, Tuple, Optional, cast import re # --- Units and Sizes --- MediaQueryCheck = Literal["max-width", "min-width", "max-height", "min-height"] Unit = Literal["px", "em", "rem", "%", "vh", "vw"] Size = Tuple[float, Unit] MediaQuery = Tuple[MediaQueryCheck, float, Unit] # --- Colors --- RGB = Tuple[float, float, float] RGBA = Tuple[float, float, float, float] Color = Union[RGB, RGBA] CSSValue = Union[Size, Color, str] # --- Positions --- PositionKeyword = Literal["static", "relative", "absolute", "fixed", "sticky"] # --- Display --- Display = Literal["none", "block", "inline", "inline-block", "flex", "grid"] # --- Alignment --- TextAlign = Literal["left", "right", "center", "justify", "start", "end"] # --- Overflow --- Overflow = Literal["visible", "hidden", "scroll", "auto", "clip"] # --- Border Style --- BorderStyle = Literal["none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"] # --- Length / Size or keyword --- AutoSize = Union[Size, Literal["auto"]] class CSS(TypedDict, total=False): color: Color backgroundColor: Color fontSize: Size margin: Union[AutoSize, Tuple[AutoSize, AutoSize], Tuple[AutoSize, AutoSize, AutoSize, AutoSize]] padding: Union[AutoSize, Tuple[AutoSize, AutoSize], Tuple[AutoSize, AutoSize, AutoSize, AutoSize]] width: AutoSize height: AutoSize display: Display position: PositionKeyword top: AutoSize left: AutoSize right: AutoSize bottom: AutoSize textAlign: TextAlign overflow: Overflow borderColor: Color borderWidth: Size borderStyle: BorderStyle opacity: float # 0.0 to 1.0 zIndex: int lineHeight: Union[float, Size] def Styles( color: Optional[Color] = None, backgroundColor: Optional[Color] = None, fontSize: Optional[Size] = None, margin: Optional[Union[AutoSize, Tuple[AutoSize, AutoSize], Tuple[AutoSize, AutoSize, AutoSize, AutoSize]]] = None, padding: Optional[Union[AutoSize, Tuple[AutoSize, AutoSize], Tuple[AutoSize, AutoSize, AutoSize, AutoSize]]] = None, width: Optional[AutoSize] = None, height: Optional[AutoSize] = None, display: Optional[Display] = None, position: Optional[PositionKeyword] = None, top: Optional[AutoSize] = None, left: Optional[AutoSize] = None, right: Optional[AutoSize] = None, bottom: Optional[AutoSize] = None, textAlign: Optional[TextAlign] = None, overflow: Optional[Overflow] = None, borderColor: Optional[Color] = None, borderWidth: Optional[Size] = None, borderStyle: Optional[BorderStyle] = None, opacity: Optional[float] = None, zIndex: Optional[int] = None, lineHeight: Optional[Union[float, Size]] = None, ) -> CSS: """ Function alternative to create a block of CSS styles. """ return cast(CSS, {k: v for k, v in locals().items() if v is not None}) def Sheet(mq:MediaQuery, *css:CSS): """ Serialize a CSS sheet with a media query and multiple CSS dictionaries. """ lines:list[str] = [] lines.append(f"@media ({mq[0]}: {mq[1]}{mq[2]}) {{") def camel_to_kebab(name): # Insert hyphen before capital letters and convert to lowercase s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1-\2', name) kebab = re.sub(r'([a-z0-9])([A-Z])', r'\1-\2', s1).lower() return kebab def concat(joiner:str, tuple:Union[RGB, RGBA, Size])->str: return joiner.join(str(item) for item in tuple) merged:CSS = {} for d in css: merged = {**merged, **d} for key, value in merged.items(): text = value if isinstance(value, tuple): size = len(value) if size < 3: text = concat("", value) elif size == 3: text = "rgb(" + concat(", ", value) + ")" elif size == 4: text = "rgba(" + concat(", ", value) + ")" lines.append(f" {camel_to_kebab(key)}: {text};") lines.append("}") return "\n".join(lines) test = Sheet( ("max-width", 1024, "px"), { "fontSize":(16, "px"), "color":(255, 0, 0), "backgroundColor":(0, 255, 0), "display":"block", }, Styles( color=(0, 0, 255), backgroundColor=(255, 255, 0), ) ) print(test)