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'') + + +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_content) - # Find all HTML elements - pattern = r'<(h[1-6]|p)(?:[^>]*)>(.*?)|' - 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]