|
|
@ -18,6 +18,7 @@ class ElementType(Enum): |
|
|
|
H5 = "h5" |
|
|
|
H5 = "h5" |
|
|
|
H6 = "h6" |
|
|
|
H6 = "h6" |
|
|
|
P = "p" |
|
|
|
P = "p" |
|
|
|
|
|
|
|
LI = "li" |
|
|
|
BR = "br" |
|
|
|
BR = "br" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -31,6 +32,7 @@ class HtmlElement: |
|
|
|
margin_top: int |
|
|
|
margin_top: int |
|
|
|
margin_bottom: int |
|
|
|
margin_bottom: int |
|
|
|
line_height: float = 1.2 |
|
|
|
line_height: float = 1.2 |
|
|
|
|
|
|
|
indent_level: int = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HtmlRenderer(Widget): |
|
|
|
class HtmlRenderer(Widget): |
|
|
@ -39,6 +41,7 @@ class HtmlRenderer(Widget): |
|
|
|
self.elements: list[HtmlElement] = [] |
|
|
|
self.elements: list[HtmlElement] = [] |
|
|
|
self._normal_font = gui_app.font(FontWeight.NORMAL) |
|
|
|
self._normal_font = gui_app.font(FontWeight.NORMAL) |
|
|
|
self._bold_font = gui_app.font(FontWeight.BOLD) |
|
|
|
self._bold_font = gui_app.font(FontWeight.BOLD) |
|
|
|
|
|
|
|
self._list_indent_px = 40 |
|
|
|
|
|
|
|
|
|
|
|
self.styles: dict[ElementType, dict[str, Any]] = { |
|
|
|
self.styles: dict[ElementType, dict[str, Any]] = { |
|
|
|
ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 16}, |
|
|
|
ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 16}, |
|
|
@ -48,6 +51,7 @@ class HtmlRenderer(Widget): |
|
|
|
ElementType.H5: {"size": 44, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 12, "margin_bottom": 6}, |
|
|
|
ElementType.H5: {"size": 44, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 12, "margin_bottom": 6}, |
|
|
|
ElementType.H6: {"size": 40, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 10, "margin_bottom": 4}, |
|
|
|
ElementType.H6: {"size": 40, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 10, "margin_bottom": 4}, |
|
|
|
ElementType.P: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 8, "margin_bottom": 12}, |
|
|
|
ElementType.P: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 8, "margin_bottom": 12}, |
|
|
|
|
|
|
|
ElementType.LI: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 6, "margin_bottom": 6}, |
|
|
|
ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "color": rl.BLACK, "margin_top": 0, "margin_bottom": 12}, |
|
|
|
ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "color": rl.BLACK, "margin_top": 0, "margin_bottom": 12}, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -72,30 +76,40 @@ class HtmlRenderer(Widget): |
|
|
|
# Remove DOCTYPE, html, head, body tags but keep their content |
|
|
|
# Remove DOCTYPE, html, head, body tags but keep their content |
|
|
|
html_content = re.sub(r'<!DOCTYPE[^>]*>', '', html_content) |
|
|
|
html_content = re.sub(r'<!DOCTYPE[^>]*>', '', html_content) |
|
|
|
html_content = re.sub(r'</?(?:html|head|body)[^>]*>', '', html_content) |
|
|
|
html_content = re.sub(r'</?(?:html|head|body)[^>]*>', '', html_content) |
|
|
|
|
|
|
|
# Keep UL tags to track nesting/indent |
|
|
|
|
|
|
|
|
|
|
|
# Find all HTML elements |
|
|
|
# Find all HTML elements |
|
|
|
pattern = r'<(h[1-6]|p)(?:[^>]*)>(.*?)</\1>|<br\s*/?>' |
|
|
|
pattern = r'(<ul[^>]*>|</ul>)|<(h[1-6]|p|li)(?:[^>]*)>(.*?)</\2>|<br\s*/?>' |
|
|
|
matches = re.finditer(pattern, html_content, re.DOTALL | re.IGNORECASE) |
|
|
|
indent_level = 0 |
|
|
|
|
|
|
|
for match in re.finditer(pattern, html_content, re.DOTALL | re.IGNORECASE): |
|
|
|
for match in matches: |
|
|
|
whole = match.group(0) |
|
|
|
if match.group(0).lower().startswith('<br'): |
|
|
|
ul_tag = match.group(1) |
|
|
|
|
|
|
|
tag = match.group(2) |
|
|
|
|
|
|
|
content = match.group(3) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if whole.lower().startswith('<br'): |
|
|
|
# Handle <br> tags |
|
|
|
# Handle <br> tags |
|
|
|
self._add_element(ElementType.BR, "") |
|
|
|
self._add_element(ElementType.BR, "") |
|
|
|
|
|
|
|
elif ul_tag is not None: |
|
|
|
|
|
|
|
if ul_tag.lower().startswith('<ul'): |
|
|
|
|
|
|
|
indent_level += 1 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
indent_level = max(0, indent_level - 1) |
|
|
|
else: |
|
|
|
else: |
|
|
|
tag = match.group(1).lower() |
|
|
|
t = tag.lower() |
|
|
|
content = match.group(2).strip() |
|
|
|
txt = (content or '').strip() |
|
|
|
|
|
|
|
txt = re.sub(r'\s+', ' ', txt).strip() |
|
|
|
# Clean up content - remove extra whitespace |
|
|
|
if txt: |
|
|
|
content = re.sub(r'\s+', ' ', content) |
|
|
|
element_type = ElementType(t) |
|
|
|
content = content.strip() |
|
|
|
self._add_element(element_type, txt, indent_level=(indent_level if element_type == ElementType.LI else max(0, indent_level - 1))) |
|
|
|
|
|
|
|
|
|
|
|
if content: # Only add non-empty elements |
|
|
|
def _add_element(self, element_type: ElementType, content: str, indent_level: int = 0) -> None: |
|
|
|
element_type = ElementType(tag) |
|
|
|
|
|
|
|
self._add_element(element_type, content) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _add_element(self, element_type: ElementType, content: str) -> None: |
|
|
|
|
|
|
|
style = self.styles[element_type] |
|
|
|
style = self.styles[element_type] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Simple bullet support for list items |
|
|
|
|
|
|
|
if element_type == ElementType.LI: |
|
|
|
|
|
|
|
content = f"• {content}" |
|
|
|
|
|
|
|
|
|
|
|
element = HtmlElement( |
|
|
|
element = HtmlElement( |
|
|
|
type=element_type, |
|
|
|
type=element_type, |
|
|
|
content=content, |
|
|
|
content=content, |
|
|
@ -104,6 +118,7 @@ class HtmlRenderer(Widget): |
|
|
|
color=style["color"], |
|
|
|
color=style["color"], |
|
|
|
margin_top=style["margin_top"], |
|
|
|
margin_top=style["margin_top"], |
|
|
|
margin_bottom=style["margin_bottom"], |
|
|
|
margin_bottom=style["margin_bottom"], |
|
|
|
|
|
|
|
indent_level=indent_level, |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
self.elements.append(element) |
|
|
|
self.elements.append(element) |
|
|
@ -116,7 +131,7 @@ class HtmlRenderer(Widget): |
|
|
|
def _render_content(self, rect: rl.Rectangle, scroll_offset: float = 0) -> float: |
|
|
|
def _render_content(self, rect: rl.Rectangle, scroll_offset: float = 0) -> float: |
|
|
|
current_y = rect.y + scroll_offset |
|
|
|
current_y = rect.y + scroll_offset |
|
|
|
padding = 20 |
|
|
|
padding = 20 |
|
|
|
content_width = rect.width - (padding * 2) |
|
|
|
base_content_width = rect.width - (padding * 2) |
|
|
|
|
|
|
|
|
|
|
|
for element in self.elements: |
|
|
|
for element in self.elements: |
|
|
|
if element.type == ElementType.BR: |
|
|
|
if element.type == ElementType.BR: |
|
|
@ -129,7 +144,8 @@ class HtmlRenderer(Widget): |
|
|
|
|
|
|
|
|
|
|
|
if element.content: |
|
|
|
if element.content: |
|
|
|
font = self._get_font(element.font_weight) |
|
|
|
font = self._get_font(element.font_weight) |
|
|
|
wrapped_lines = wrap_text(font, element.content, element.font_size, int(content_width)) |
|
|
|
indent_px = (element.indent_level * self._list_indent_px) if element.type == ElementType.LI else 0 |
|
|
|
|
|
|
|
wrapped_lines = wrap_text(font, element.content, element.font_size, int(base_content_width - indent_px)) |
|
|
|
|
|
|
|
|
|
|
|
for line in wrapped_lines: |
|
|
|
for line in wrapped_lines: |
|
|
|
if current_y < rect.y - element.font_size: |
|
|
|
if current_y < rect.y - element.font_size: |
|
|
@ -139,7 +155,7 @@ class HtmlRenderer(Widget): |
|
|
|
if current_y > rect.y + rect.height: |
|
|
|
if current_y > rect.y + rect.height: |
|
|
|
break |
|
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
rl.draw_text_ex(font, line, rl.Vector2(rect.x + padding, current_y), element.font_size, 0, rl.WHITE) |
|
|
|
rl.draw_text_ex(font, line, rl.Vector2(rect.x + padding + indent_px, current_y), element.font_size, 0, rl.WHITE) |
|
|
|
|
|
|
|
|
|
|
|
current_y += element.font_size * element.line_height |
|
|
|
current_y += element.font_size * element.line_height |
|
|
|
|
|
|
|
|
|
|
@ -162,7 +178,8 @@ class HtmlRenderer(Widget): |
|
|
|
|
|
|
|
|
|
|
|
if element.content: |
|
|
|
if element.content: |
|
|
|
font = self._get_font(element.font_weight) |
|
|
|
font = self._get_font(element.font_weight) |
|
|
|
wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width)) |
|
|
|
indent_px = (element.indent_level * self._list_indent_px) if element.type == ElementType.LI else 0 |
|
|
|
|
|
|
|
wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width - indent_px)) |
|
|
|
|
|
|
|
|
|
|
|
for _ in wrapped_lines: |
|
|
|
for _ in wrapped_lines: |
|
|
|
total_height += element.font_size * element.line_height |
|
|
|
total_height += element.font_size * element.line_height |
|
|
|