"""µ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, ) @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}: `" 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"`" elif isinstance(node, FormButton): return f"`[`!{node.button_label}`!`:{node.dest}]" return "" def compile(source: str, width: int = 64) -> 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 # 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) # 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, )