from __future__ import annotations
import xml.dom.minidom as minidom
from string import Formatter
from typing import Any
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, underline and strike.
HTML('...')
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(f"{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 f"HTML({self.value!r})"
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.
"""
return HTML(FORMATTER.vformat(self.value, args, kwargs))
def __mod__(self, value: 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)
class HTMLFormatter(Formatter):
def format_field(self, value: object, format_spec: str) -> str:
return html_escape(format(value, format_spec))
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 = f"{text}"
return (
text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
)
FORMATTER = HTMLFormatter()