diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 2ca5ccd5cc..6dcd991f99 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -9,6 +9,7 @@ from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle +LIST_INDENT_PX = 40 class ElementType(Enum): H1 = "h1" @@ -18,9 +19,23 @@ class ElementType(Enum): H5 = "h5" H6 = "h6" P = "p" + UL = "ul" + LI = "li" BR = "br" +TAG_NAMES = '|'.join([t.value for t in ElementType]) +START_TAG_RE = re.compile(f'<({TAG_NAMES})>') +END_TAG_RE = re.compile(f'({TAG_NAMES})>') + + +def is_tag(token: str) -> tuple[bool, bool, ElementType | None]: + supported_tag = bool(START_TAG_RE.fullmatch(token)) + supported_end_tag = bool(END_TAG_RE.fullmatch(token)) + tag = ElementType(token[1:-1].strip('/')) if supported_tag or supported_end_tag else None + return supported_tag, supported_end_tag, tag + + @dataclass class HtmlElement: type: ElementType @@ -30,6 +45,7 @@ class HtmlElement: margin_top: int margin_bottom: int line_height: float = 1.2 + indent_level: int = 0 class HtmlRenderer(Widget): @@ -40,10 +56,12 @@ class HtmlRenderer(Widget): self.elements: list[HtmlElement] = [] self._normal_font = gui_app.font(FontWeight.NORMAL) self._bold_font = gui_app.font(FontWeight.BOLD) + self._indent_level = 0 if text_size is None: text_size = {} + # Untagged text defaults to
self.styles: dict[ElementType, dict[str, Any]] = {
ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "margin_top": 20, "margin_bottom": 16},
ElementType.H2: {"size": 60, "weight": FontWeight.BOLD, "margin_top": 24, "margin_bottom": 12},
@@ -77,25 +95,51 @@ class HtmlRenderer(Widget):
html_content = re.sub(r']*>', '', html_content)
html_content = re.sub(r'?(?:html|head|body)[^>]*>', '', html_content)
- # Find all HTML elements
- pattern = r'<(h[1-6]|p)(?:[^>]*)>(.*?)\1>|
'
- matches = re.finditer(pattern, html_content, re.DOTALL | re.IGNORECASE)
+ # Parse HTML
+ tokens = re.findall(r'[^>]+>|<[^>]+>|[^<\s]+', html_content)
- for match in matches:
- if match.group(0).lower().startswith('
tags
- self._add_element(ElementType.BR, "")
- else:
- tag = match.group(1).lower()
- content = match.group(2).strip()
+ def close_tag():
+ nonlocal current_content
+ nonlocal current_tag
+
+ # If no tag is set, default to paragraph so we don't lose text
+ if current_tag is None:
+ current_tag = ElementType.P
+
+ text = ' '.join(current_content).strip()
+ current_content = []
+ if text:
+ if current_tag == ElementType.LI:
+ text = '• ' + text
+ self._add_element(current_tag, text)
- # Clean up content - remove extra whitespace
- content = re.sub(r'\s+', ' ', content)
- content = content.strip()
+ current_content: list[str] = []
+ current_tag: ElementType | None = None
+ for token in tokens:
+ is_start_tag, is_end_tag, tag = is_tag(token)
+ if tag is not None:
+ if tag == ElementType.BR:
+ self._add_element(ElementType.BR, "")
+
+ elif tag == ElementType.UL:
+ self._indent_level = self._indent_level + 1 if is_start_tag else max(0, self._indent_level - 1)
+
+ # elif is_start_tag:
+ # current_tag = tag
+
+ elif is_start_tag or is_end_tag:
+ # Always add content regardless of opening or closing tag
+ close_tag()
+
+ # TODO: reset to None if end tag?
+ if is_start_tag:
+ current_tag = tag
+
+ else:
+ current_content.append(token)
- if content: # Only add non-empty elements
- element_type = ElementType(tag)
- self._add_element(element_type, content)
+ if current_content:
+ close_tag()
def _add_element(self, element_type: ElementType, content: str) -> None:
style = self.styles[element_type]