from random import choices as random_choices from string import ascii_lowercase from typing import Any, Callable 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 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)