150 lines
4.6 KiB
Python
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,
|
|
)
|