1from __future__ import annotations 
    2 
    3import xml.dom.minidom as minidom 
    4from string import Formatter 
    5from typing import Any 
    6 
    7from .base import FormattedText, StyleAndTextTuples 
    8 
    9__all__ = ["HTML"] 
    10 
    11 
    12class HTML: 
    13    """ 
    14    HTML formatted text. 
    15    Take something HTML-like, for use as a formatted string. 
    16 
    17    :: 
    18 
    19        # Turn something into red. 
    20        HTML('<style fg="ansired" bg="#00ff44">...</style>') 
    21 
    22        # Italic, bold, underline and strike. 
    23        HTML('<i>...</i>') 
    24        HTML('<b>...</b>') 
    25        HTML('<u>...</u>') 
    26        HTML('<s>...</s>') 
    27 
    28    All HTML elements become available as a "class" in the style sheet. 
    29    E.g. ``<username>...</username>`` can be styled, by setting a style for 
    30    ``username``. 
    31    """ 
    32 
    33    def __init__(self, value: str) -> None: 
    34        self.value = value 
    35        document = minidom.parseString(f"<html-root>{value}</html-root>") 
    36 
    37        result: StyleAndTextTuples = [] 
    38        name_stack: list[str] = [] 
    39        fg_stack: list[str] = [] 
    40        bg_stack: list[str] = [] 
    41 
    42        def get_current_style() -> str: 
    43            "Build style string for current node." 
    44            parts = [] 
    45            if name_stack: 
    46                parts.append("class:" + ",".join(name_stack)) 
    47 
    48            if fg_stack: 
    49                parts.append("fg:" + fg_stack[-1]) 
    50            if bg_stack: 
    51                parts.append("bg:" + bg_stack[-1]) 
    52            return " ".join(parts) 
    53 
    54        def process_node(node: Any) -> None: 
    55            "Process node recursively." 
    56            for child in node.childNodes: 
    57                if child.nodeType == child.TEXT_NODE: 
    58                    result.append((get_current_style(), child.data)) 
    59                else: 
    60                    add_to_name_stack = child.nodeName not in ( 
    61                        "#document", 
    62                        "html-root", 
    63                        "style", 
    64                    ) 
    65                    fg = bg = "" 
    66 
    67                    for k, v in child.attributes.items(): 
    68                        if k == "fg": 
    69                            fg = v 
    70                        if k == "bg": 
    71                            bg = v 
    72                        if k == "color": 
    73                            fg = v  # Alias for 'fg'. 
    74 
    75                    # Check for spaces in attributes. This would result in 
    76                    # invalid style strings otherwise. 
    77                    if " " in fg: 
    78                        raise ValueError('"fg" attribute contains a space.') 
    79                    if " " in bg: 
    80                        raise ValueError('"bg" attribute contains a space.') 
    81 
    82                    if add_to_name_stack: 
    83                        name_stack.append(child.nodeName) 
    84                    if fg: 
    85                        fg_stack.append(fg) 
    86                    if bg: 
    87                        bg_stack.append(bg) 
    88 
    89                    process_node(child) 
    90 
    91                    if add_to_name_stack: 
    92                        name_stack.pop() 
    93                    if fg: 
    94                        fg_stack.pop() 
    95                    if bg: 
    96                        bg_stack.pop() 
    97 
    98        process_node(document) 
    99 
    100        self.formatted_text = FormattedText(result) 
    101 
    102    def __repr__(self) -> str: 
    103        return f"HTML({self.value!r})" 
    104 
    105    def __pt_formatted_text__(self) -> StyleAndTextTuples: 
    106        return self.formatted_text 
    107 
    108    def format(self, *args: object, **kwargs: object) -> HTML: 
    109        """ 
    110        Like `str.format`, but make sure that the arguments are properly 
    111        escaped. 
    112        """ 
    113        return HTML(FORMATTER.vformat(self.value, args, kwargs)) 
    114 
    115    def __mod__(self, value: object) -> HTML: 
    116        """ 
    117        HTML('<b>%s</b>') % value 
    118        """ 
    119        if not isinstance(value, tuple): 
    120            value = (value,) 
    121 
    122        value = tuple(html_escape(i) for i in value) 
    123        return HTML(self.value % value) 
    124 
    125 
    126class HTMLFormatter(Formatter): 
    127    def format_field(self, value: object, format_spec: str) -> str: 
    128        return html_escape(format(value, format_spec)) 
    129 
    130 
    131def html_escape(text: object) -> str: 
    132    # The string interpolation functions also take integers and other types. 
    133    # Convert to string first. 
    134    if not isinstance(text, str): 
    135        text = f"{text}" 
    136 
    137    return ( 
    138        text.replace("&", "&") 
    139        .replace("<", "<") 
    140        .replace(">", ">") 
    141        .replace('"', """) 
    142    ) 
    143 
    144 
    145FORMATTER = HTMLFormatter()