Files
Browser/ren_browser/renderer/micron.py
2025-11-16 00:46:42 -06:00

294 lines
8.5 KiB
Python

"""Micron markup renderer for Ren Browser.
Provides rendering capabilities for micron markup content.
"""
import re
import flet as ft
from ren_browser.renderer.plaintext import render_plaintext
def hex_to_rgb(hex_color: str) -> str:
"""Convert 3-char hex color to RGB string."""
if len(hex_color) != 3:
return "255,255,255"
r = int(hex_color[0], 16) * 17
g = int(hex_color[1], 16) * 17
b = int(hex_color[2], 16) * 17
return f"{r},{g},{b}"
def parse_micron_line(line: str) -> list:
"""Parse a single line of micron markup into styled text spans.
Returns list of dicts with 'text', 'bold', 'italic', 'underline', 'color', 'bgcolor'.
"""
spans = []
current_text = ""
bold = False
italic = False
underline = False
color = None
bgcolor = None
i = 0
while i < len(line):
if line[i] == "`" and i + 1 < len(line):
if current_text:
spans.append(
{
"text": current_text,
"bold": bold,
"italic": italic,
"underline": underline,
"color": color,
"bgcolor": bgcolor,
}
)
current_text = ""
tag = line[i + 1]
if tag == "!":
bold = not bold
i += 2
elif tag == "*":
italic = not italic
i += 2
elif tag == "_":
underline = not underline
i += 2
elif tag == "F" and i + 5 <= len(line):
color = hex_to_rgb(line[i + 2 : i + 5])
i += 5
elif tag == "f":
color = None
i += 2
elif tag == "B" and i + 5 <= len(line):
bgcolor = hex_to_rgb(line[i + 2 : i + 5])
i += 5
elif tag == "b":
bgcolor = None
i += 2
elif tag == "`":
bold = False
italic = False
underline = False
color = None
bgcolor = None
i += 2
else:
current_text += line[i]
i += 1
else:
current_text += line[i]
i += 1
if current_text:
spans.append(
{
"text": current_text,
"bold": bold,
"italic": italic,
"underline": underline,
"color": color,
"bgcolor": bgcolor,
}
)
return spans
def render_micron(content: str, on_link_click=None) -> ft.Control:
"""Render micron markup content to a Flet control.
Falls back to plaintext renderer if parsing fails.
Args:
content: Micron markup content to render.
on_link_click: Optional callback function(url) called when a link is clicked.
Returns:
ft.Control: Rendered content as a Flet control.
"""
try:
return _render_micron_internal(content, on_link_click)
except Exception as e:
print(f"Micron rendering failed: {e}, falling back to plaintext")
return render_plaintext(content)
def _render_micron_internal(content: str, on_link_click=None) -> ft.Control:
"""Internal micron rendering implementation.
Args:
content: Micron markup content to render.
on_link_click: Optional callback function(url) called when a link is clicked.
Returns:
ft.Control: Rendered content as a Flet control.
"""
lines = content.split("\n")
controls = []
section_level = 0
alignment = ft.TextAlign.LEFT
for line in lines:
if not line:
controls.append(ft.Container(height=10))
continue
if line.startswith("#"):
continue
if line.startswith("`c"):
alignment = ft.TextAlign.CENTER
line = line[2:]
elif line.startswith("`l"):
alignment = ft.TextAlign.LEFT
line = line[2:]
elif line.startswith("`r"):
alignment = ft.TextAlign.RIGHT
line = line[2:]
elif line.startswith("`a"):
alignment = ft.TextAlign.LEFT
line = line[2:]
if line.startswith(">"):
level = 0
while level < len(line) and line[level] == ">":
level += 1
section_level = level
heading_text = line[level:].strip()
if heading_text:
controls.append(
ft.Container(
content=ft.Text(
heading_text,
size=20 - (level * 2),
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_400,
),
padding=ft.padding.only(left=level * 20, top=10, bottom=5),
),
)
continue
if line.strip() == "-":
controls.append(
ft.Container(
content=ft.Divider(color=ft.Colors.GREY_700),
padding=ft.padding.only(left=section_level * 20),
),
)
continue
if "`[" in line:
row_controls = []
last_end = 0
for link_match in re.finditer(r"`\[([^`]*)`([^\]]*)\]", line):
before = line[last_end : link_match.start()]
if before:
before_spans = parse_micron_line(before)
for span in before_spans:
row_controls.append(create_text_span(span))
label = link_match.group(1)
url = link_match.group(2)
def make_link_handler(link_url):
def handler(e):
if on_link_click:
on_link_click(link_url)
return handler
row_controls.append(
ft.TextButton(
text=label if label else url,
style=ft.ButtonStyle(
color=ft.Colors.BLUE_400,
overlay_color=ft.Colors.BLUE_900,
),
on_click=make_link_handler(url),
),
)
last_end = link_match.end()
after = line[last_end:]
if after:
after_spans = parse_micron_line(after)
for span in after_spans:
row_controls.append(create_text_span(span))
if row_controls:
controls.append(
ft.Container(
content=ft.Row(
controls=row_controls,
spacing=0,
wrap=True,
),
padding=ft.padding.only(left=section_level * 20),
),
)
continue
spans = parse_micron_line(line)
if spans:
text_controls = [create_text_span(span) for span in spans]
controls.append(
ft.Container(
content=ft.Row(
controls=text_controls,
spacing=0,
wrap=True,
alignment=alignment,
),
padding=ft.padding.only(left=section_level * 20),
),
)
return ft.Column(
controls=controls,
spacing=5,
scroll=ft.ScrollMode.AUTO,
expand=True,
)
def create_text_span(span: dict) -> ft.Text:
"""Create a Text control from a span dict."""
styles = []
if span["bold"]:
styles.append(ft.TextStyle(weight=ft.FontWeight.BOLD))
if span["italic"]:
styles.append(ft.TextStyle(italic=True))
text_decoration = ft.TextDecoration.UNDERLINE if span["underline"] else None
color = span["color"]
bgcolor = span["bgcolor"]
text_style = ft.TextStyle(
weight=ft.FontWeight.BOLD if span["bold"] else None,
italic=span["italic"] if span["italic"] else None,
decoration=text_decoration,
)
return ft.Text(
span["text"],
style=text_style,
color=f"rgb({color})" if color else None,
bgcolor=f"rgb({bgcolor})" if bgcolor else None,
selectable=True,
no_wrap=False,
)