import xml.dom.minidom as minidom from typing import Any, List, Tuple, Union from .base import FormattedText, StyleAndTextTuples __all__ = ["HTML"] class HTML: """ HTML formatted text. Take something HTML-like, for use as a formatted string. :: # Turn something into red. HTML('') # Italic, bold and underline. HTML('...') HTML('...') HTML('...') All HTML elements become available as a "class" in the style sheet. E.g. ``...`` can be styled, by setting a style for ``username``. """ def __init__(self, value: str) -> None: self.value = value document = minidom.parseString("%s" % (value,)) result: StyleAndTextTuples = [] name_stack: List[str] = [] fg_stack: List[str] = [] bg_stack: List[str] = [] def get_current_style() -> str: " Build style string for current node. " parts = [] if name_stack: parts.append("class:" + ",".join(name_stack)) if fg_stack: parts.append("fg:" + fg_stack[-1]) if bg_stack: parts.append("bg:" + bg_stack[-1]) return " ".join(parts) def process_node(node: Any) -> None: " Process node recursively. " for child in node.childNodes: if child.nodeType == child.TEXT_NODE: result.append((get_current_style(), child.data)) else: add_to_name_stack = child.nodeName not in ( "#document", "html-root", "style", ) fg = bg = "" for k, v in child.attributes.items(): if k == "fg": fg = v if k == "bg": bg = v if k == "color": fg = v # Alias for 'fg'. # Check for spaces in attributes. This would result in # invalid style strings otherwise. if " " in fg: raise ValueError('"fg" attribute contains a space.') if " " in bg: raise ValueError('"bg" attribute contains a space.') if add_to_name_stack: name_stack.append(child.nodeName) if fg: fg_stack.append(fg) if bg: bg_stack.append(bg) process_node(child) if add_to_name_stack: name_stack.pop() if fg: fg_stack.pop() if bg: bg_stack.pop() process_node(document) self.formatted_text = FormattedText(result) def __repr__(self) -> str: return "HTML(%r)" % (self.value,) def __pt_formatted_text__(self) -> StyleAndTextTuples: return self.formatted_text def format(self, *args: object, **kwargs: object) -> "HTML": """ Like `str.format`, but make sure that the arguments are properly escaped. """ # Escape all the arguments. escaped_args = [html_escape(a) for a in args] escaped_kwargs = {k: html_escape(v) for k, v in kwargs.items()} return HTML(self.value.format(*escaped_args, **escaped_kwargs)) def __mod__(self, value: Union[object, Tuple[object, ...]]) -> "HTML": """ HTML('%s') % value """ if not isinstance(value, tuple): value = (value,) value = tuple(html_escape(i) for i in value) return HTML(self.value % value) def html_escape(text: object) -> str: # The string interpolation functions also take integers and other types. # Convert to string first. if not isinstance(text, str): text = "{}".format(text) return ( text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) )