from random import choices as random_choices from string import ascii_lowercase from typing import Any, Callable from django.template import TemplateDoesNotExist from django.template.loader import render_to_string from django.urls import NoReverseMatch, reverse from django.utils.safestring import SafeText, mark_safe from common.utils import truncate HTMLAttribute = tuple[str, str | int | bool] HTMLTag = str def Component( attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], template: str = "", tag_name: str = "", ) -> HTMLTag: if not tag_name and not template: raise ValueError("One of template or tag_name is required.") if isinstance(children, str): children = [children] childrenBlob = "\n".join(children) if len(attributes) == 0: attributesBlob = "" else: attributesList = [f'{name}="{value}"' for name, value in attributes] # make attribute list into a string # and insert space between tag and attribute list attributesBlob = f" {" ".join(attributesList)}" tag: str = "" if tag_name != "": tag = f"<{tag_name}{attributesBlob}>{childrenBlob}" elif template != "": tag = render_to_string( template, {name: value for name, value in attributes} | {"slot": mark_safe("\n".join(children))}, ) return mark_safe(tag) def randomid(seed: str = "", length: int = 10) -> str: return seed + "".join(random_choices(ascii_lowercase, k=length)) def Popover( popover_content: str, wrapped_content: str = "", children: list[HTMLTag] = [], attributes: list[HTMLAttribute] = [], ) -> str: if not wrapped_content and not children: raise ValueError("One of wrapped_content or children is required.") id = randomid() return Component( attributes=attributes + [ ("id", id), ("wrapped_content", wrapped_content), ("popover_content", popover_content), ], children=children, template="cotton/popover.html", ) def PopoverTruncated(input_string: str) -> str: if (truncated := truncate(input_string)) != input_string: return Popover(wrapped_content=truncated, popover_content=input_string) else: return input_string def A( attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], url: str | Callable[..., Any] = "", ): """ Returns the HTML tag "a". "url" can either be: - URL (string) - path name passed to reverse() (string) - function """ additional_attributes = [] if url: if type(url) is str: try: url_result = reverse(url) except NoReverseMatch: url_result = url elif callable(url): url_result = url() else: raise TypeError("'url' is neither str nor function.") additional_attributes = [("href", url_result)] return Component( tag_name="a", attributes=attributes + additional_attributes, children=children ) def Button( attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], size: str = "base", icon: bool = False, color: str = "blue", ): return Component( template="cotton/button.html", attributes=attributes + [("size", size), ("icon", icon), ("color", color)], children=children, ) def Div( attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], ): return Component(tag_name="div", attributes=attributes, children=children) def Label( attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], ): return Component(tag_name="label", attributes=attributes, children=children) def Input( type: str = "text", label: str = "", id: str = "", attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], ): input_component = Component( tag_name="input", attributes=attributes + [("type", type), ("id", id)], children=children, ) if label != "": if id == "": raise ValueError("Label is set but element ID is missing.") return Label( attributes=[("for", id)], children=[label, input_component, *children] ) else: return input_component def Form( action="", method="get", attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], ): return Component( tag_name="form", attributes=attributes + [("action", action), ("method", method)], children=children, ) def Fieldset( label: str = "", attributes: list[HTMLAttribute] = [], children: list[HTMLTag] | HTMLTag = [], ): if label != "": children = [Label(children=[label, *children])] return Component(tag_name="fieldset", attributes=attributes, children=children) def RadioFieldset(name: str, label: str, radio_buttons: list[dict[str, str]]): return Component( tag_name="span", children=[ Component(tag_name="legend", children=label), Component( tag_name="fieldset", children=[ Component( tag_name="label", attributes=[ ("for", f"{name}__{radio["value"]}"), ], children=[ radio["label"], Input( type="radio", attributes=[ ("id", f"{name}__{radio["value"]}"), ("name", name), ("value", radio["value"]), ("onClick", radio.get("onclick", "")), ], ), ], ) for radio in radio_buttons ], ), ], ) def BooleanRadioFieldset(name: str, label: str): return RadioFieldset( name=name, label=label, radio_buttons=[ {"label": "True", "value": "true"}, {"label": "False", "value": "false"}, ], ) def SubmitButton(label: str): return Input(type="submit", attributes=[("value", label)]) # RadioFieldset( # name="filter__dropped", # label="Dropped", # radio_buttons=[ # {"label": "True", "value": "true"}, # {"label": "False", "value": "false"}, # ], # ) def Icon( name: str, attributes: list[HTMLAttribute] = [], ): try: result = Component(template=f"cotton/icon/{name}.html", attributes=attributes) except TemplateDoesNotExist: result = Icon(name="unspecified", attributes=attributes) return result def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText: link = reverse("view_game", args=[int(game_id)]) a_content = Div( [("class", "inline-flex gap-2 items-center")], [ Icon( platform.icon, [("title", platform.name)], ), PopoverTruncated(name), ], ) return mark_safe( A( url=link, children=[a_content], ), ) def NameWithPlatformIcon(name: str, platform: str) -> SafeText: content = Div( [("class", "inline-flex gap-2 items-center")], [ Icon( platform.icon, [("title", platform.name)], ), PopoverTruncated(name), ], ) return mark_safe(content)