Files
micronomicon/backend/uframe/__init__.py
2026-04-01 10:45:02 +02:00

150 lines
4.6 KiB
Python

"""µFrame — A DSL for rich terminal UIs rendered as ASCII and Micron.
Public API:
compile(source, width=64) → CompileResult
"""
from __future__ import annotations
from dataclasses import dataclass, field
from uframe.errors import CompileWarning, UFrameError
from uframe.parser import parse
from uframe.measure import measure
from uframe.layout import layout
from uframe.paint import paint
from uframe.borders import merge_borders
from uframe.grid import CharGrid
from uframe.emit_ascii import emit_ascii
from uframe.emit_micron import emit_micron
from uframe.ir import (
IRNode, Field, Password, Radio, Checkbox, FormButton,
Source, IfBlock, ForLoop, OnSubmit, StateDecl, CacheControl,
)
from uframe.themes import get_theme, ThemeDef
@dataclass
class CompileResult:
"""Result of compiling a .uf source."""
ascii: str = ""
micron: str = ""
script: str = "" # generated Python script (dynamic mode only)
is_dynamic: bool = False
warnings: list[CompileWarning] = field(default_factory=list)
def _has_dynamic_nodes(node: IRNode) -> bool:
"""Check if the IR tree contains any dynamic nodes."""
if isinstance(node, (Source, IfBlock, ForLoop, OnSubmit, StateDecl, CacheControl)):
return True
return any(_has_dynamic_nodes(child) for child in node.children)
def _collect_form_nodes(node: IRNode) -> dict[int, IRNode]:
"""Walk the IR tree and collect form nodes keyed by their y position."""
result: dict[int, IRNode] = {}
if isinstance(node, (Field, Password, Radio, Checkbox, FormButton)):
result[node.rect.y] = node
for child in node.children:
result.update(_collect_form_nodes(child))
return result
def _micron_form_line(node: IRNode) -> str:
"""Generate the Micron form tag for a form node."""
if isinstance(node, Field):
w = node.field_width
name = node.field_name
ph = node.placeholder or name
return f"{name}: `<{w}|{name}`{ph}>"
elif isinstance(node, Password):
w = node.field_width
name = node.field_name
ph = node.placeholder or name
return f"{name}: `<!{w}|{name}`{ph}>"
elif isinstance(node, Radio):
parts = []
for i, opt in enumerate(node.options):
val = opt.lower().replace(" ", "_")
checked = "|*" if i == 0 else ""
parts.append(f"`<^|{node.group}|{val}{checked}`{opt}>")
return f"{node.group}: {' '.join(parts)}"
elif isinstance(node, Checkbox):
name = node.field_name
label = node.checkbox_label
checked = "|*" if node.checked else ""
return f"`<?|{name}|yes{checked}`{label}>"
elif isinstance(node, FormButton):
return f"`[`!{node.button_label}`!`:{node.dest}]"
return ""
def compile(source: str, width: int = 64, theme: str = "") -> CompileResult:
"""Compile a µFrame .uf source string into ASCII and Micron output.
Args:
source: the .uf DSL source text
width: page width in characters (default 64)
Returns:
CompileResult with ascii, micron, and any warnings
Raises:
ParseError: if the source cannot be parsed
LayoutError: if layout constraints fail
"""
warnings: list[CompileWarning] = []
# 1. Parse
page = parse(source)
if page.width == 64 and width != 64:
page.width = width
w = page.width
# 1b. Resolve theme (CLI flag overrides source directive)
theme_name = theme or page.theme_name or "default"
theme_def = get_theme(theme_name)
# 2. Measure
measure(page, w)
# 3. Layout
total_h = layout(page, 0, 0, w, page.pref_height + 100)
# 4. Create grid and paint
grid = CharGrid(w, max(total_h, 1))
paint(page, grid, theme_def)
# 5. Merge borders
merge_borders(grid)
# 6. Emit
ascii_out = emit_ascii(grid)
micron_out = emit_micron(grid, page_title=page.title)
# 7. Post-pass: replace form element lines in Micron output
form_nodes = _collect_form_nodes(page)
if form_nodes:
micron_lines = micron_out.split("\n")
for row_y, form_node in form_nodes.items():
if 0 <= row_y < len(micron_lines):
micron_lines[row_y] = _micron_form_line(form_node)
micron_out = "\n".join(micron_lines)
# 8. Check if this page has dynamic features
is_dynamic = _has_dynamic_nodes(page)
script = ""
if is_dynamic:
from uframe.codegen import compile_dynamic
script = compile_dynamic(page)
return CompileResult(
ascii=ascii_out,
micron=micron_out,
script=script,
is_dynamic=is_dynamic,
warnings=warnings,
)