Add Micron renderer #3
@@ -4,24 +4,540 @@ Provides rendering capabilities for micron markup content,
|
||||
currently implemented as a placeholder.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
def render_micron(content: str) -> ft.Control:
|
||||
"""Render micron markup content to a Flet control placeholder.
|
||||
class MicronParser:
|
||||
"""Parses micron markup and converts it to Flet controls.
|
||||
|
||||
Currently displays raw content.
|
||||
Supports headings, dividers, inline formatting, ASCII art detection,
|
||||
and color/formatting codes.
|
||||
"""
|
||||
|
||||
def __init__(self, dark_theme=True, enable_force_monospace=True, ascii_art_scale=0.75):
|
||||
"""Initialize the MicronParser.
|
||||
|
||||
Args:
|
||||
dark_theme (bool): Whether to use dark theme styles.
|
||||
enable_force_monospace (bool): If True, force monospace font.
|
||||
ascii_art_scale (float): Scale factor for ASCII art font size.
|
||||
|
||||
"""
|
||||
self.dark_theme = dark_theme
|
||||
self.enable_force_monospace = enable_force_monospace
|
||||
self.ascii_art_scale = ascii_art_scale
|
||||
self.DEFAULT_FG_DARK = "ddd"
|
||||
self.DEFAULT_FG_LIGHT = "222"
|
||||
self.DEFAULT_BG = "default"
|
||||
|
||||
self.SELECTED_STYLES = None
|
||||
|
||||
self.STYLES_DARK = {
|
||||
"plain": {"fg": self.DEFAULT_FG_DARK, "bg": self.DEFAULT_BG, "bold": False, "underline": False, "italic": False},
|
||||
"heading1": {"fg": "000", "bg": "bbb", "bold": True, "underline": False, "italic": False},
|
||||
"heading2": {"fg": "000", "bg": "999", "bold": True, "underline": False, "italic": False},
|
||||
"heading3": {"fg": "fff", "bg": "777", "bold": True, "underline": False, "italic": False},
|
||||
}
|
||||
|
||||
self.STYLES_LIGHT = {
|
||||
"plain": {"fg": self.DEFAULT_FG_LIGHT, "bg": self.DEFAULT_BG, "bold": False, "underline": False, "italic": False},
|
||||
"heading1": {"fg": "fff", "bg": "777", "bold": True, "underline": False, "italic": False},
|
||||
"heading2": {"fg": "000", "bg": "aaa", "bold": True, "underline": False, "italic": False},
|
||||
"heading3": {"fg": "000", "bg": "ccc", "bold": True, "underline": False, "italic": False},
|
||||
}
|
||||
|
||||
if self.dark_theme:
|
||||
self.SELECTED_STYLES = self.STYLES_DARK
|
||||
else:
|
||||
self.SELECTED_STYLES = self.STYLES_LIGHT
|
||||
|
||||
def convert_micron_to_controls(self, markup: str) -> list[ft.Control]:
|
||||
"""Convert micron markup to a list of Flet controls.
|
||||
|
||||
Args:
|
||||
markup (str): The micron markup string.
|
||||
|
||||
Returns:
|
||||
list[ft.Control]: List of Flet controls representing the markup.
|
||||
|
||||
"""
|
||||
controls = []
|
||||
state = self._init_state()
|
||||
lines = markup.split("\n")
|
||||
|
||||
for line in lines:
|
||||
line_controls = self._parse_line(line, state)
|
||||
if line_controls:
|
||||
controls.extend(line_controls)
|
||||
|
||||
return controls
|
||||
|
||||
def _init_state(self) -> dict:
|
||||
"""Initialize the parsing state for a new document.
|
||||
|
||||
Returns:
|
||||
dict: The initial state dictionary.
|
||||
|
||||
"""
|
||||
return {
|
||||
"literal": False,
|
||||
"depth": 0,
|
||||
"fg_color": self.SELECTED_STYLES["plain"]["fg"],
|
||||
"bg_color": self.DEFAULT_BG,
|
||||
"formatting": {
|
||||
"bold": False,
|
||||
"underline": False,
|
||||
"italic": False,
|
||||
},
|
||||
"default_align": "left",
|
||||
"align": "left",
|
||||
}
|
||||
|
||||
def _parse_line(self, line: str, state: dict) -> list[ft.Control]:
|
||||
"""Parse a single line of micron markup.
|
||||
|
||||
Args:
|
||||
line (str): The line to parse.
|
||||
state (dict): The current parsing state.
|
||||
|
||||
Returns:
|
||||
list[ft.Control]: Controls for this line, or empty if none.
|
||||
|
||||
"""
|
||||
if not line:
|
||||
return []
|
||||
|
||||
if line == "`=":
|
||||
state["literal"] = not state["literal"]
|
||||
return []
|
||||
|
||||
if not state["literal"] and line.startswith("#"):
|
||||
return []
|
||||
|
||||
if not state["literal"] and line.startswith("<"):
|
||||
state["depth"] = 0
|
||||
return self._parse_line(line[1:], state)
|
||||
|
||||
if not state["literal"] and line.startswith(">"):
|
||||
return self._parse_heading(line, state)
|
||||
|
||||
if not state["literal"] and line.startswith("-"):
|
||||
return self._parse_divider(line, state)
|
||||
|
||||
return self._parse_inline_formatting(line, state)
|
||||
|
||||
def _parse_inline_formatting(self, line: str, state: dict) -> list[ft.Control]:
|
||||
"""Parse inline formatting codes in a line and return Flet controls.
|
||||
|
||||
Args:
|
||||
line (str): The line to parse.
|
||||
state (dict): The current parsing state.
|
||||
|
||||
Returns:
|
||||
list[ft.Control]: Controls for the formatted line.
|
||||
|
||||
"""
|
||||
spans = []
|
||||
current_text = ""
|
||||
current_style = {
|
||||
"fg": state["fg_color"],
|
||||
"bg": state["bg_color"],
|
||||
"bold": state["formatting"]["bold"],
|
||||
"underline": state["formatting"]["underline"],
|
||||
"italic": state["formatting"]["italic"],
|
||||
}
|
||||
|
||||
mode = "text"
|
||||
i = 0
|
||||
skip = 0
|
||||
|
||||
def flush_current():
|
||||
"""Flush the current text buffer into a span."""
|
||||
nonlocal current_text
|
||||
if current_text:
|
||||
spans.append(MicronParser._create_span(current_text, current_style))
|
||||
current_text = ""
|
||||
|
||||
while i < len(line):
|
||||
if skip > 0:
|
||||
skip -= 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
char = line[i]
|
||||
"""
|
||||
Handle backticks for formatting:
|
||||
- Double backtick (``) resets formatting and alignment.
|
||||
- Single backtick toggles formatting mode.
|
||||
"""
|
||||
if char == "`":
|
||||
flush_current()
|
||||
if i + 1 < len(line) and line[i + 1] == "`":
|
||||
current_style["bold"] = False
|
||||
current_style["underline"] = False
|
||||
current_style["italic"] = False
|
||||
current_style["fg"] = self.SELECTED_STYLES["plain"]["fg"]
|
||||
current_style["bg"] = self.DEFAULT_BG
|
||||
state["align"] = state["default_align"]
|
||||
i += 2
|
||||
continue
|
||||
mode = "formatting" if mode == "text" else "text"
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if mode == "formatting":
|
||||
handled = False
|
||||
if char == "_":
|
||||
current_style["underline"] = not current_style["underline"]
|
||||
handled = True
|
||||
elif char == "!":
|
||||
current_style["bold"] = not current_style["bold"]
|
||||
handled = True
|
||||
elif char == "*":
|
||||
current_style["italic"] = not current_style["italic"]
|
||||
handled = True
|
||||
elif char == "F":
|
||||
if len(line) >= i + 4:
|
||||
current_style["fg"] = line[i + 1:i + 4]
|
||||
skip = 3
|
||||
handled = True
|
||||
elif char == "f":
|
||||
current_style["fg"] = self.SELECTED_STYLES["plain"]["fg"]
|
||||
handled = True
|
||||
elif char == "B":
|
||||
if len(line) >= i + 4:
|
||||
current_style["bg"] = line[i + 1:i + 4]
|
||||
skip = 3
|
||||
handled = True
|
||||
elif char == "b":
|
||||
current_style["bg"] = self.DEFAULT_BG
|
||||
handled = True
|
||||
elif char == "c":
|
||||
state["align"] = "center"
|
||||
handled = True
|
||||
elif char == "l":
|
||||
state["align"] = "left"
|
||||
handled = True
|
||||
elif char == "r":
|
||||
state["align"] = "right"
|
||||
handled = True
|
||||
elif char == "a":
|
||||
state["align"] = state["default_align"]
|
||||
handled = True
|
||||
|
||||
if not handled:
|
||||
current_text += char
|
||||
|
||||
else:
|
||||
current_text += char
|
||||
i += 1
|
||||
|
||||
flush_current()
|
||||
|
||||
if spans:
|
||||
is_art = MicronParser._is_ascii_art("".join(span.text for span in spans))
|
||||
font_size = 12 * self.ascii_art_scale if is_art else None
|
||||
text_control = ft.Text(spans=spans, text_align=state["align"], selectable=True, enable_interactive_selection=True, expand=True, font_family="monospace", size=font_size)
|
||||
else:
|
||||
is_art = MicronParser._is_ascii_art(line)
|
||||
font_size = 12 * self.ascii_art_scale if is_art else None
|
||||
text_control = ft.Text(line, text_align=state["align"], selectable=True, enable_interactive_selection=True, expand=True, font_family="monospace", size=font_size)
|
||||
|
||||
if state["depth"] > 0:
|
||||
indent_em = (state["depth"] - 1) * 1.2
|
||||
text_control = ft.Container(
|
||||
content=text_control,
|
||||
margin=ft.margin.only(left=indent_em * 16),
|
||||
)
|
||||
|
||||
return [text_control]
|
||||
|
||||
@staticmethod
|
||||
def _create_span(text: str, style: dict) -> ft.TextSpan:
|
||||
"""Create a Flet TextSpan with the given style.
|
||||
|
||||
Args:
|
||||
text (str): The text for the span.
|
||||
style (dict): The style dictionary.
|
||||
|
||||
Returns:
|
||||
ft.TextSpan: The styled text span.
|
||||
|
||||
"""
|
||||
flet_style = ft.TextStyle(
|
||||
color=MicronParser._color_to_flet(style["fg"]),
|
||||
bgcolor=MicronParser._color_to_flet(style["bg"]),
|
||||
weight=ft.FontWeight.BOLD if style["bold"] else ft.FontWeight.NORMAL,
|
||||
decoration=ft.TextDecoration.UNDERLINE if style["underline"] else ft.TextDecoration.NONE,
|
||||
italic=style["italic"],
|
||||
)
|
||||
return ft.TextSpan(text, flet_style)
|
||||
|
||||
def _apply_format_code_to_style(self, code: str, style: dict, state: dict):
|
||||
"""Apply a micron format code to a style dictionary.
|
||||
|
||||
Args:
|
||||
code (str): The format code.
|
||||
style (dict): The style dictionary to modify.
|
||||
state (dict): The current parsing state.
|
||||
|
||||
"""
|
||||
if not code:
|
||||
return
|
||||
|
||||
if code == "`":
|
||||
style["bold"] = False
|
||||
style["underline"] = False
|
||||
style["italic"] = False
|
||||
style["fg"] = self.SELECTED_STYLES["plain"]["fg"]
|
||||
style["bg"] = self.DEFAULT_BG
|
||||
return
|
||||
|
||||
if "!" in code:
|
||||
style["bold"] = not style["bold"]
|
||||
if "_" in code:
|
||||
style["underline"] = not style["underline"]
|
||||
if "*" in code:
|
||||
style["italic"] = not style["italic"]
|
||||
|
||||
if code.startswith("F") and len(code) >= 4:
|
||||
style["fg"] = code[1:4]
|
||||
elif code.startswith("B") and len(code) >= 4:
|
||||
style["bg"] = code[1:4]
|
||||
|
||||
if "f" in code:
|
||||
style["fg"] = self.SELECTED_STYLES["plain"]["fg"]
|
||||
if "b" in code:
|
||||
style["bg"] = self.DEFAULT_BG
|
||||
|
||||
def _apply_format_code(self, code: str, state: dict):
|
||||
"""Apply a micron format code to the parsing state.
|
||||
|
||||
Args:
|
||||
code (str): The format code.
|
||||
state (dict): The state dictionary to modify.
|
||||
|
||||
"""
|
||||
if not code:
|
||||
return
|
||||
|
||||
if code == "`":
|
||||
state["formatting"]["bold"] = False
|
||||
state["formatting"]["underline"] = False
|
||||
state["formatting"]["italic"] = False
|
||||
state["fg_color"] = self.SELECTED_STYLES["plain"]["fg"]
|
||||
state["bg_color"] = self.DEFAULT_BG
|
||||
state["align"] = state["default_align"]
|
||||
return
|
||||
|
||||
if "!" in code:
|
||||
state["formatting"]["bold"] = not state["formatting"]["bold"]
|
||||
if "_" in code:
|
||||
state["formatting"]["underline"] = not state["formatting"]["underline"]
|
||||
if "*" in code:
|
||||
state["formatting"]["italic"] = not state["formatting"]["italic"]
|
||||
|
||||
if code.startswith("F") and len(code) >= 4:
|
||||
state["fg_color"] = code[1:4]
|
||||
elif code.startswith("B") and len(code) >= 4:
|
||||
state["bg_color"] = code[1:4]
|
||||
|
||||
if "f" in code:
|
||||
state["fg_color"] = self.SELECTED_STYLES["plain"]["fg"]
|
||||
if "b" in code:
|
||||
state["bg_color"] = self.DEFAULT_BG
|
||||
|
||||
if "c" in code:
|
||||
state["align"] = "center"
|
||||
elif "l" in code:
|
||||
state["align"] = "left"
|
||||
elif "r" in code:
|
||||
state["align"] = "right"
|
||||
elif "a" in code:
|
||||
state["align"] = state["default_align"]
|
||||
|
||||
def _parse_divider(self, line: str, state: dict) -> list[ft.Control]:
|
||||
"""Parse a divider line and return a Flet Divider or styled Text.
|
||||
|
||||
Args:
|
||||
line (str): The divider line.
|
||||
state (dict): The current parsing state.
|
||||
|
||||
Returns:
|
||||
list[ft.Control]: Controls for the divider.
|
||||
|
||||
"""
|
||||
if len(line) == 1:
|
||||
return [ft.Divider()]
|
||||
divider_char = line[1] if len(line) > 1 else "-"
|
||||
repeated = divider_char * 80
|
||||
|
||||
is_art = MicronParser._is_ascii_art(repeated)
|
||||
font_size = 12 * self.ascii_art_scale if is_art else None
|
||||
|
||||
divider = ft.Text(
|
||||
repeated,
|
||||
font_family="monospace",
|
||||
color=MicronParser._color_to_flet(state["fg_color"]),
|
||||
bgcolor=MicronParser._color_to_flet(state["bg_color"]),
|
||||
no_wrap=True,
|
||||
overflow=ft.TextOverflow.CLIP,
|
||||
selectable=False,
|
||||
enable_interactive_selection=False,
|
||||
size=font_size,
|
||||
)
|
||||
|
||||
return [divider]
|
||||
|
||||
@staticmethod
|
||||
def _color_to_flet(color: str) -> str | None:
|
||||
"""Convert micron color format to Flet color format.
|
||||
|
||||
Args:
|
||||
color (str): The micron color string.
|
||||
|
||||
Returns:
|
||||
str | None: The Flet color string or None if default/invalid.
|
||||
|
||||
"""
|
||||
if not color or color == "default":
|
||||
return None
|
||||
|
||||
if len(color) == 3 and re.match(r"^[0-9a-fA-F]{3}$", color):
|
||||
return f"#{color[0]*2}{color[1]*2}{color[2]*2}"
|
||||
|
||||
if len(color) == 6 and re.match(r"^[0-9a-fA-F]{6}$", color):
|
||||
return f"#{color}"
|
||||
|
||||
if len(color) == 3 and color[0] == "g":
|
||||
try:
|
||||
val = int(color[1:])
|
||||
if 0 <= val <= 99:
|
||||
h = hex(int(val * 2.55))[2:].zfill(2)
|
||||
return f"#{h}{h}{h}"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _is_ascii_art(text: str) -> bool:
|
||||
"""Detect if text appears to be ASCII art.
|
||||
|
||||
Args:
|
||||
text (str): The text to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the text is likely ASCII art, False otherwise.
|
||||
|
||||
"""
|
||||
if not text or len(text) < 10:
|
||||
return False
|
||||
|
||||
special_chars = set("│─┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬█▄▀▌▐■□▪▫▲▼◄►◆◇○●◎◢◣◥◤")
|
||||
special_count = sum(1 for char in text if char in special_chars or (ord(char) > 127))
|
||||
|
||||
other_special = sum(1 for char in text if not char.isalnum() and char not in " \t")
|
||||
|
||||
total_chars = len(text.replace(" ", "").replace("\t", ""))
|
||||
if total_chars == 0:
|
||||
return False
|
||||
|
||||
special_ratio = (special_count + other_special) / total_chars
|
||||
return special_ratio > 0.3
|
||||
|
||||
def _parse_heading(self, line: str, state: dict) -> list[ft.Control]:
|
||||
"""Parse heading lines (starting with '>') and return styled controls.
|
||||
|
||||
Args:
|
||||
line (str): The heading line.
|
||||
state (dict): The current parsing state.
|
||||
|
||||
Returns:
|
||||
list[ft.Control]: Controls for the heading.
|
||||
|
||||
"""
|
||||
heading_level = 0
|
||||
for char in line:
|
||||
if char == ">":
|
||||
heading_level += 1
|
||||
else:
|
||||
break
|
||||
|
||||
state["depth"] = heading_level
|
||||
|
||||
heading_text = line[heading_level:].strip()
|
||||
|
||||
if heading_text:
|
||||
style_key = f"heading{min(heading_level, 3)}"
|
||||
style = self.SELECTED_STYLES.get(style_key, self.SELECTED_STYLES["plain"])
|
||||
|
||||
is_art = MicronParser._is_ascii_art(heading_text)
|
||||
base_size = 20 - heading_level * 2
|
||||
font_size = base_size * self.ascii_art_scale if is_art else base_size
|
||||
|
||||
indent_em = max(0, (state["depth"] - 1) * 1.2)
|
||||
|
||||
heading = ft.Text(
|
||||
heading_text,
|
||||
style=ft.TextStyle(
|
||||
color=MicronParser._color_to_flet(style["fg"]),
|
||||
weight=ft.FontWeight.BOLD if style["bold"] else ft.FontWeight.NORMAL,
|
||||
size=font_size,
|
||||
),
|
||||
selectable=True,
|
||||
enable_interactive_selection=True,
|
||||
expand=True,
|
||||
font_family="monospace",
|
||||
)
|
||||
|
||||
bg_color = MicronParser._color_to_flet(style["bg"])
|
||||
if bg_color:
|
||||
heading = ft.Container(
|
||||
content=ft.Container(
|
||||
content=heading,
|
||||
margin=ft.margin.only(left=indent_em * 16) if indent_em > 0 else None,
|
||||
padding=ft.padding.symmetric(horizontal=4),
|
||||
),
|
||||
bgcolor=bg_color,
|
||||
width=float("inf"),
|
||||
)
|
||||
elif indent_em > 0:
|
||||
heading = ft.Container(
|
||||
content=heading,
|
||||
margin=ft.margin.only(left=indent_em * 16),
|
||||
padding=ft.padding.symmetric(horizontal=4),
|
||||
)
|
||||
|
||||
return [heading]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def render_micron(content: str, ascii_art_scale: float = 0.75) -> ft.Control:
|
||||
"""Render micron markup content to a Flet control.
|
||||
|
||||
Args:
|
||||
content: Micron markup content to render.
|
||||
ascii_art_scale: Scale factor for ASCII art (0.0-1.0). Default 0.75.
|
||||
|
||||
Returns:
|
||||
ft.Control: Rendered content as a Flet control.
|
||||
|
||||
This function parses the micron markup, merges adjacent text controls
|
||||
with the same style, and returns a Flet ListView containing the result.
|
||||
|
||||
"""
|
||||
return ft.Text(
|
||||
content,
|
||||
selectable=True,
|
||||
font_family="monospace",
|
||||
parser = MicronParser(ascii_art_scale=ascii_art_scale)
|
||||
controls = parser.convert_micron_to_controls(content)
|
||||
|
||||
return ft.Container(
|
||||
content=ft.ListView(
|
||||
controls=controls,
|
||||
spacing=2,
|
||||
expand=True,
|
||||
),
|
||||
expand=True,
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ def sample_page_request():
|
||||
from ren_browser.pages.page_request import PageRequest
|
||||
|
||||
return PageRequest(
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestAnnounce:
|
||||
def test_announce_with_none_display_name(self):
|
||||
"""Test Announce creation with None display name."""
|
||||
announce = Announce(
|
||||
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890
|
||||
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890,
|
||||
)
|
||||
|
||||
assert announce.destination_hash == "1234567890abcdef"
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestLogsModule:
|
||||
assert len(logs.RET_LOGS) == 1
|
||||
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
|
||||
logs._original_rns_log.assert_called_once_with(
|
||||
"Test RNS message", "arg1", kwarg1="value1"
|
||||
"Test RNS message", "arg1", kwarg1="value1",
|
||||
)
|
||||
assert result == "original_result"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class TestPageRequest:
|
||||
def test_page_request_creation(self):
|
||||
"""Test basic PageRequest creation."""
|
||||
request = PageRequest(
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu"
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu",
|
||||
)
|
||||
|
||||
assert request.destination_hash == "1234567890abcdef"
|
||||
|
||||
@@ -58,53 +58,303 @@ class TestPlaintextRenderer:
|
||||
class TestMicronRenderer:
|
||||
"""Test cases for the micron renderer.
|
||||
|
||||
Note: The micron renderer is currently a placeholder implementation
|
||||
that displays raw content without markup processing.
|
||||
The micron renderer parses Micron markup format and returns a ListView
|
||||
containing styled controls with proper formatting, colors, and layout.
|
||||
"""
|
||||
|
||||
def test_render_micron_basic(self):
|
||||
"""Test basic micron rendering (currently displays raw content)."""
|
||||
"""Test basic micron rendering."""
|
||||
content = "# Heading\n\nSome content"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Text)
|
||||
assert result.value == "# Heading\n\nSome content"
|
||||
assert result.selectable is True
|
||||
assert result.font_family == "monospace"
|
||||
assert result.expand is True
|
||||
# Should return a Container with ListView
|
||||
assert isinstance(result, ft.Container)
|
||||
assert isinstance(result.content, ft.ListView)
|
||||
assert result.content.spacing == 2
|
||||
|
||||
# Should contain controls
|
||||
assert len(result.content.controls) > 0
|
||||
for control in result.content.controls:
|
||||
assert control.selectable is True
|
||||
assert control.font_family == "monospace"
|
||||
|
||||
def test_render_micron_empty(self):
|
||||
"""Test micron rendering with empty content."""
|
||||
content = ""
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Text)
|
||||
assert result.value == ""
|
||||
assert result.selectable is True
|
||||
# Should return a Container
|
||||
assert isinstance(result, ft.Container)
|
||||
|
||||
# May contain empty controls
|
||||
|
||||
def test_render_micron_unicode(self):
|
||||
"""Test micron rendering with Unicode characters."""
|
||||
content = "Unicode content: 你好 🌍 αβγ"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Text)
|
||||
assert result.value == content
|
||||
assert result.selectable is True
|
||||
# Should return a Container
|
||||
assert isinstance(result, ft.Container)
|
||||
|
||||
# Should contain controls with the content
|
||||
assert len(result.content.controls) > 0
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
# Extract text from the control
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
elif isinstance(control, ft.Container) and hasattr(control, "content"):
|
||||
# Handle indented text controls
|
||||
if isinstance(control.content, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control.content, "value") and control.content.value:
|
||||
text_content = control.content.value
|
||||
elif hasattr(control.content, "spans") and control.content.spans:
|
||||
text_content = "".join(span.text for span in control.content.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
|
||||
# Remove trailing newline and should preserve the content
|
||||
all_text = all_text.rstrip("\n")
|
||||
assert content in all_text
|
||||
|
||||
def test_render_micron_headings(self):
|
||||
"""Test micron rendering with different heading levels."""
|
||||
content = "> Level 1\n>> Level 2\n>>> Level 3"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
assert len(result.content.controls) == 3
|
||||
|
||||
# Check that headings are wrapped in containers with backgrounds
|
||||
for control in result.content.controls:
|
||||
assert isinstance(control, ft.Container)
|
||||
assert control.bgcolor is not None # Should have background color
|
||||
assert control.width == float("inf") # Should be full width
|
||||
|
||||
def test_render_micron_formatting(self):
|
||||
"""Test micron rendering with text formatting."""
|
||||
content = "`!Bold text!` and `_underline_` and `*italic*`"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
assert len(result.content.controls) >= 1
|
||||
|
||||
# Should produce some text content
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
|
||||
assert len(all_text.strip()) > 0 # Should have some processed content
|
||||
|
||||
def test_render_micron_colors(self):
|
||||
"""Test micron rendering with color codes."""
|
||||
content = "`FffRed text` and `B00Blue background`"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
assert len(result.content.controls) >= 1
|
||||
|
||||
# Should produce some text content (color codes may consume characters)
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
|
||||
assert len(all_text.strip()) > 0 # Should have some processed content
|
||||
|
||||
def test_render_micron_alignment(self):
|
||||
"""Test micron rendering with alignment."""
|
||||
content = "`cCentered text`"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
assert len(result.content.controls) >= 1
|
||||
|
||||
# Should have some text content
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
|
||||
assert len(all_text.strip()) > 0
|
||||
|
||||
def test_render_micron_comments(self):
|
||||
"""Test that comments are ignored."""
|
||||
content = "# This is a comment\nVisible text"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
# Should only contain the visible text, not the comment
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
|
||||
all_text = all_text.strip()
|
||||
assert "Visible text" in all_text
|
||||
assert "This is a comment" not in all_text
|
||||
|
||||
def test_render_micron_section_depth(self):
|
||||
"""Test micron rendering with section depth/indentation."""
|
||||
content = "> Main section\n>> Subsection\n>>> Sub-subsection"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
assert len(result.content.controls) == 3
|
||||
|
||||
# Check indentation increases with depth
|
||||
for i, control in enumerate(result.content.controls):
|
||||
assert isinstance(control, ft.Container)
|
||||
# The inner container should have margin for indentation
|
||||
inner_container = control.content
|
||||
if hasattr(inner_container, "margin") and inner_container.margin:
|
||||
# Should have left margin based on depth: (depth-1) * 1.2 * 16
|
||||
# depth = i + 1, so margin = i * 1.2 * 16
|
||||
expected_margin = i * 1.2 * 16 # 19.2px per depth level above 1
|
||||
assert inner_container.margin.left == expected_margin
|
||||
|
||||
def test_render_micron_ascii_art(self):
|
||||
"""Test micron rendering with ASCII art scaling."""
|
||||
# Create content with ASCII art characters
|
||||
ascii_art = "┌───┐\n│Box│\n└───┘"
|
||||
content = f"Normal text\n{ascii_art}\nMore text"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
# Each line is kept as separate control
|
||||
assert len(result.content.controls) >= 3
|
||||
# Should contain the ASCII art content
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
all_text = all_text.strip()
|
||||
assert "┌───┐" in all_text
|
||||
assert "Normal text" in all_text
|
||||
|
||||
def test_render_micron_literal_mode(self):
|
||||
"""Test micron literal mode."""
|
||||
content = "`=Literal mode`\n# This should be visible\n`=Back to normal`"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
# Should contain the processed content (literal mode may not be fully implemented)
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
all_text += text_content + "\n"
|
||||
|
||||
# At minimum, should contain some text content
|
||||
assert len(all_text.strip()) > 0
|
||||
|
||||
def test_render_micron_dividers(self):
|
||||
"""Test micron rendering with dividers."""
|
||||
content = "Text above\n-\nText below"
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
# Should contain controls for text
|
||||
assert len(result.content.controls) >= 2
|
||||
|
||||
def test_render_micron_complex_formatting(self):
|
||||
"""Test complex combination of micron formatting."""
|
||||
content = """# Comment (ignored)
|
||||
> Heading
|
||||
Regular text.
|
||||
|
||||
>> Subsection
|
||||
Centered text
|
||||
Final paragraph."""
|
||||
|
||||
result = render_micron(content)
|
||||
|
||||
assert isinstance(result, ft.Container)
|
||||
assert len(result.content.controls) >= 3 # Should have multiple elements
|
||||
|
||||
# Check for heading containers
|
||||
heading_containers = [c for c in result.content.controls if isinstance(c, ft.Container)]
|
||||
assert len(heading_containers) >= 2 # At least 2 headings
|
||||
|
||||
# Check that we have some text content
|
||||
def extract_all_text(control):
|
||||
"""Recursively extract text from control and its children."""
|
||||
text = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text += control.value
|
||||
elif hasattr(control, "_Control__attrs") and "value" in control._Control__attrs:
|
||||
text += control._Control__attrs["value"][0]
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text += "".join(span.text for span in control.spans)
|
||||
elif hasattr(control, "content"):
|
||||
text += extract_all_text(control.content)
|
||||
return text
|
||||
|
||||
all_text = ""
|
||||
for control in result.content.controls:
|
||||
all_text += extract_all_text(control)
|
||||
|
||||
assert "Heading" in all_text
|
||||
assert "Subsection" in all_text
|
||||
assert "Regular text" in all_text
|
||||
|
||||
|
||||
class TestRendererComparison:
|
||||
"""Test cases comparing both renderers."""
|
||||
|
||||
def test_renderers_return_same_type(self):
|
||||
"""Test that both renderers return the same control type."""
|
||||
def test_renderers_return_correct_types(self):
|
||||
"""Test that both renderers return the expected control types."""
|
||||
content = "Test content"
|
||||
|
||||
plaintext_result = render_plaintext(content)
|
||||
micron_result = render_micron(content)
|
||||
|
||||
assert type(plaintext_result) is type(micron_result)
|
||||
# Plaintext returns Text, Micron returns Container
|
||||
assert isinstance(plaintext_result, ft.Text)
|
||||
assert isinstance(micron_result, ft.Text)
|
||||
assert isinstance(micron_result, ft.Container)
|
||||
|
||||
def test_renderers_preserve_content(self):
|
||||
"""Test that both renderers preserve the original content."""
|
||||
@@ -114,7 +364,33 @@ class TestRendererComparison:
|
||||
micron_result = render_micron(content)
|
||||
|
||||
assert plaintext_result.value == content
|
||||
assert micron_result.value == content
|
||||
|
||||
# For micron result (Container), extract text from controls
|
||||
micron_text = ""
|
||||
for control in micron_result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
# Extract text from the control
|
||||
text_content = ""
|
||||
if hasattr(control, "value") and control.value:
|
||||
text_content = control.value
|
||||
elif hasattr(control, "spans") and control.spans:
|
||||
text_content = "".join(span.text for span in control.spans)
|
||||
if text_content:
|
||||
micron_text += text_content + "\n"
|
||||
elif isinstance(control, ft.Container) and hasattr(control, "content"):
|
||||
# Handle indented text controls
|
||||
if isinstance(control.content, ft.Text):
|
||||
text_content = ""
|
||||
if hasattr(control.content, "value") and control.content.value:
|
||||
text_content = control.content.value
|
||||
elif hasattr(control.content, "spans") and control.content.spans:
|
||||
text_content = "".join(span.text for span in control.content.spans)
|
||||
if text_content:
|
||||
micron_text += text_content + "\n"
|
||||
|
||||
# Remove trailing newline and compare
|
||||
micron_text = micron_text.rstrip("\n")
|
||||
assert micron_text == content
|
||||
|
||||
def test_renderers_same_properties(self):
|
||||
"""Test that both renderers set the same basic properties."""
|
||||
@@ -123,6 +399,17 @@ class TestRendererComparison:
|
||||
plaintext_result = render_plaintext(content)
|
||||
micron_result = render_micron(content)
|
||||
|
||||
assert plaintext_result.selectable == micron_result.selectable
|
||||
assert plaintext_result.font_family == micron_result.font_family
|
||||
assert plaintext_result.expand == micron_result.expand
|
||||
# Check basic properties
|
||||
assert plaintext_result.selectable is True
|
||||
assert plaintext_result.font_family == "monospace"
|
||||
assert plaintext_result.expand is True
|
||||
|
||||
# For micron result (Container), check properties
|
||||
assert isinstance(micron_result.content, ft.ListView)
|
||||
assert micron_result.content.spacing == 2
|
||||
|
||||
# Check that all Text controls in the ListView have the expected properties
|
||||
for control in micron_result.content.controls:
|
||||
if isinstance(control, ft.Text):
|
||||
assert control.selectable is True
|
||||
assert control.font_family == "monospace"
|
||||
|
||||
@@ -18,7 +18,7 @@ class TestStorageManager:
|
||||
def test_storage_manager_init_without_page(self):
|
||||
"""Test StorageManager initialization without a page."""
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory",
|
||||
) as mock_get_dir:
|
||||
mock_dir = Path("/mock/storage")
|
||||
mock_get_dir.return_value = mock_dir
|
||||
@@ -35,7 +35,7 @@ class TestStorageManager:
|
||||
mock_page = Mock()
|
||||
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory",
|
||||
) as mock_get_dir:
|
||||
mock_dir = Path("/mock/storage")
|
||||
mock_get_dir.return_value = mock_dir
|
||||
@@ -51,12 +51,12 @@ class TestStorageManager:
|
||||
with (
|
||||
patch("os.name", "posix"),
|
||||
patch.dict(
|
||||
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True
|
||||
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True,
|
||||
),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
):
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
|
||||
):
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = storage._get_storage_directory()
|
||||
@@ -76,7 +76,7 @@ class TestStorageManager:
|
||||
patch("pathlib.Path.mkdir"),
|
||||
):
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
|
||||
):
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = storage._get_storage_directory()
|
||||
@@ -141,7 +141,7 @@ class TestStorageManager:
|
||||
|
||||
assert result is True
|
||||
mock_page.client_storage.set.assert_called_with(
|
||||
"ren_browser_config", config_content
|
||||
"ren_browser_config", config_content,
|
||||
)
|
||||
|
||||
def test_save_config_fallback(self):
|
||||
@@ -169,7 +169,7 @@ class TestStorageManager:
|
||||
assert result is True
|
||||
# Check that the config was set to client storage
|
||||
mock_page.client_storage.set.assert_any_call(
|
||||
"ren_browser_config", config_content
|
||||
"ren_browser_config", config_content,
|
||||
)
|
||||
# Verify that client storage was called at least once
|
||||
assert mock_page.client_storage.set.call_count >= 1
|
||||
@@ -240,7 +240,7 @@ class TestStorageManager:
|
||||
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
||||
assert bookmarks_path.exists()
|
||||
|
||||
with open(bookmarks_path, "r", encoding="utf-8") as f:
|
||||
with open(bookmarks_path, encoding="utf-8") as f:
|
||||
loaded_bookmarks = json.load(f)
|
||||
assert loaded_bookmarks == bookmarks
|
||||
|
||||
@@ -281,7 +281,7 @@ class TestStorageManager:
|
||||
history_path = storage._storage_dir / "history.json"
|
||||
assert history_path.exists()
|
||||
|
||||
with open(history_path, "r", encoding="utf-8") as f:
|
||||
with open(history_path, encoding="utf-8") as f:
|
||||
loaded_history = json.load(f)
|
||||
assert loaded_history == history
|
||||
|
||||
@@ -418,7 +418,7 @@ class TestStorageManagerEdgeCases:
|
||||
storage = StorageManager()
|
||||
|
||||
with patch(
|
||||
"pathlib.Path.write_text", side_effect=PermissionError("Access denied")
|
||||
"pathlib.Path.write_text", side_effect=PermissionError("Access denied"),
|
||||
):
|
||||
test_path = Path("/mock/path")
|
||||
result = storage._is_writable(test_path)
|
||||
|
||||
@@ -29,7 +29,7 @@ class TestBuildUI:
|
||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||
def test_build_ui_appbar_setup(
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page,
|
||||
):
|
||||
"""Test that build_ui sets up the app bar correctly."""
|
||||
mock_tab_manager = Mock()
|
||||
@@ -51,7 +51,7 @@ class TestBuildUI:
|
||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||
def test_build_ui_drawer_setup(
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page,
|
||||
):
|
||||
"""Test that build_ui sets up the drawer correctly."""
|
||||
mock_tab_manager = Mock()
|
||||
|
||||
Reference in New Issue
Block a user