"""Dynamic page compiler — generate executable Python scripts from µFrame IR. Takes a parsed IR tree containing dynamic nodes (source, if, for, on_submit, state) and generates a self-contained Python script that: 1. Sets shebang + cache header 2. Reads form data from environment variables 3. Executes source commands 4. Evaluates conditionals and loops 5. Renders the layout into a CharGrid 6. Emits Micron to stdout """ from __future__ import annotations import textwrap from pathlib import Path from uframe.ir import ( IRNode, Page, Box, Row, Col, Spacer, Pad, Heading, Text, Label, Divider, Link, ListNode, ListItem, Gauge, Sparkline, Status, Table, Form, Field, Password, Radio, Checkbox, FormButton, Let, Source, IfBlock, ForLoop, CacheControl, OnSubmit, StateDecl, SourceType, BorderWeight, HeadingLevel, DividerStyle, ListStyle, ) def _indent(code: str, level: int = 1) -> str: """Indent a block of code.""" prefix = " " * level return "\n".join(prefix + line if line.strip() else "" for line in code.split("\n")) def _resolve_vars(text: str) -> str: """Convert $var references to Python f-string expressions.""" import re # Replace $var.attr.attr with {var_attr_attr} and simple $var with {var} def replace_var(m: re.Match) -> str: var = m.group(1) # Replace dots with underscores for Python variable names py_var = var.replace(".", "_") return "{" + py_var + "}" return re.sub(r'\$([a-zA-Z_][\w.]*)', replace_var, text) def _emit_node(node: IRNode, indent_level: int = 0) -> list[str]: """Generate Python code lines for a single IR node.""" lines: list[str] = [] ind = " " * indent_level if isinstance(node, Let): val = node.var_value # Try to detect numeric values try: float(val) lines.append(f"{ind}{node.var_name} = {val}") except ValueError: if "," in val: # Comma-separated list lines.append(f"{ind}{node.var_name} = [{val}]") else: lines.append(f"{ind}{node.var_name} = {val!r}") elif isinstance(node, Source): var = node.var_name if node.source_type == SourceType.SHELL: lines.append(f"{ind}{var} = _shell({node.command!r}, timeout={node.timeout})") elif node.source_type == SourceType.FILE: lines.append(f"{ind}{var} = _read_file({node.command!r})") elif node.source_type == SourceType.JSON: lines.append(f"{ind}{var} = _read_json({node.command!r})") elif node.source_type == SourceType.PYTHON: lines.append(f"{ind}{var} = eval({node.command!r})") elif node.source_type == SourceType.PARAM: lines.append(f"{ind}{var} = _get_param({node.command!r})") elif node.source_type == SourceType.RNS: lines.append(f"{ind}{var} = _shell('rnstatus {node.command}', timeout={node.timeout})") elif isinstance(node, CacheControl): lines.append(f"{ind}_cache_seconds = {node.seconds}") elif isinstance(node, StateDecl): lines.append(f"{ind}{node.state_name} = _load_state({node.path!r})") elif isinstance(node, IfBlock): cond = _resolve_vars(node.condition) # Convert simple comparisons cond = cond.replace("&&", " and ").replace("||", " or ") lines.append(f"{ind}if {cond}:") if node.children: for child in node.children: lines.extend(_emit_node(child, indent_level + 1)) else: lines.append(f"{ind} pass") for elif_cond, elif_children in node.elif_branches: ec = _resolve_vars(elif_cond).replace("&&", " and ").replace("||", " or ") lines.append(f"{ind}elif {ec}:") if elif_children: for child in elif_children: lines.extend(_emit_node(child, indent_level + 1)) else: lines.append(f"{ind} pass") if node.else_children: lines.append(f"{ind}else:") for child in node.else_children: lines.extend(_emit_node(child, indent_level + 1)) elif isinstance(node, ForLoop): iterable = _resolve_vars(node.iterable) lines.append(f"{ind}for {node.var_name} in _iter({iterable}):") if node.children: for child in node.children: lines.extend(_emit_node(child, indent_level + 1)) else: lines.append(f"{ind} pass") elif isinstance(node, OnSubmit): lines.append(f"{ind}if _get_field({node.form_name!r}, ''):") lines.append(f"{ind} # Form '{node.form_name}' was submitted") for child in node.children: lines.extend(_emit_node(child, indent_level + 1)) elif isinstance(node, Page): lines.append(f"{ind}_page_title = {node.title!r}") lines.append(f"{ind}_page_width = {node.width}") lines.append(f"{ind}_uf_source_parts = []") for child in node.children: lines.extend(_emit_node(child, indent_level)) # Content nodes — emit as µFrame source that gets compiled elif isinstance(node, Heading): level = node.level.value text = _resolve_vars(node.text) lines.append(f"{ind}_uf_source_parts.append(f'heading {level} \"{text}\"')") elif isinstance(node, Text): text = _resolve_vars(node.content) lines.append(f"{ind}_uf_source_parts.append(f'text \"{text}\"')") elif isinstance(node, Label): key = _resolve_vars(node.key) val = _resolve_vars(node.value) lines.append(f"{ind}_uf_source_parts.append(f'label \"{key}\" \"{val}\"')") elif isinstance(node, Gauge): label = _resolve_vars(node.label) value = _resolve_vars(str(node.value)) if "$" in str(node.value) else str(node.value) extra = "" if node.warn is not None: extra += f" warn={node.warn}" if node.crit is not None: extra += f" crit={node.crit}" lines.append(f"{ind}_uf_source_parts.append(f'gauge \"{label}\" {value} {node.max_val} {node.bar_width}{extra}')") elif isinstance(node, Status): label = _resolve_vars(node.label) state = _resolve_vars(node.state) lines.append(f"{ind}_uf_source_parts.append(f'status \"{label}\" {state}')") elif isinstance(node, Box): weight = node.weight.name.lower() title = _resolve_vars(node.title) lines.append(f"{ind}_uf_source_parts.append(f'box {weight} \"{title}\"')") for child in node.children: # Indent children for the box child_lines = _emit_node(child, indent_level) for cl in child_lines: if "_uf_source_parts.append" in cl: # Add 2-space indent to the µFrame source cl = cl.replace(".append(f'", ".append(f' ") cl = cl.replace(".append('", ".append(' ") lines.append(cl) elif isinstance(node, Divider): style = node.divider_style.name.lower() lines.append(f"{ind}_uf_source_parts.append('divider {style}')") elif isinstance(node, Spacer): lines.append(f"{ind}_uf_source_parts.append('spacer {node.lines}')") elif isinstance(node, Link): display = _resolve_vars(node.display) dest = _resolve_vars(node.dest) lines.append(f"{ind}_uf_source_parts.append(f'link \"{display}\" \"{dest}\"')") elif isinstance(node, Field): lines.append(f"{ind}_uf_source_parts.append('field \"{node.field_name}\" {node.field_width} \"{node.placeholder}\"')") elif isinstance(node, FormButton): lines.append(f"{ind}_uf_source_parts.append('button \"{node.button_label}\" \"{node.dest}\"')") elif isinstance(node, Form): lines.append(f"{ind}_uf_source_parts.append('form \"{node.form_name}\"')") for child in node.children: child_lines = _emit_node(child, indent_level) for cl in child_lines: if "_uf_source_parts.append" in cl: cl = cl.replace(".append(f'", ".append(f' ") cl = cl.replace(".append('", ".append(' ") lines.append(cl) else: # Generic: emit children for child in node.children: lines.extend(_emit_node(child, indent_level)) return lines # --------------------------------------------------------------------------- # Runtime template embedded in generated scripts # --------------------------------------------------------------------------- def _build_script(uframe_import: str, page_logic: str, page_title: str, page_width: int) -> str: """Build the dynamic script from parts (avoids str.format brace issues).""" lines = [ "#!/usr/bin/env python3", "# Auto-generated by uFrame", "# Do not edit — regenerate with: uframe compile .uf", "", "import os, sys, json, subprocess, datetime, secrets, shlex", "", "# ─── Runtime Helpers ─────────────────────────────────────────", "", 'def _shell(cmd, timeout=5):', ' """Execute shell command, return stdout."""', ' try:', ' return subprocess.check_output(cmd, shell=True, timeout=timeout).decode().strip()', ' except Exception:', ' return ""', "", 'def _read_file(path):', ' """Read file contents."""', ' try:', ' return open(path).read().strip()', ' except Exception:', ' return ""', "", 'def _read_json(path):', ' """Read and parse JSON file."""', ' try:', ' with open(path) as f:', ' return json.load(f)', ' except Exception:', ' return {}', "", 'def _get_field(name, default=""):', ' """Read submitted form field from environment."""', ' return os.environ.get(f"FIELD_{name}", default)', "", 'def _get_param(name, default=""):', ' """Read URL parameter."""', ' return os.environ.get(f"PARAM_{name}",', ' os.environ.get(f"var_{name}", default))', "", 'def _load_state(path):', ' """Load state from JSON file."""', ' try:', ' with open(path) as f:', ' return json.load(f)', ' except Exception:', ' return {}', "", 'def _save_state(path, data):', ' """Save state to JSON file."""', ' os.makedirs(os.path.dirname(path), exist_ok=True)', ' with open(path, "w") as f:', ' json.dump(data, f, indent=2)', "", 'def _iter(val):', ' """Make a value iterable for for-loops."""', ' if isinstance(val, (list, tuple)):', ' return val', ' if isinstance(val, dict):', ' return [val]', ' if isinstance(val, str):', ' return val.strip().splitlines()', ' return []', "", f"# ─── µFrame Compile ──────────────────────────────────────────", "", uframe_import, "", "# ─── Page Logic ──────────────────────────────────────────────", "", "_cache_seconds = 0", "", page_logic, "", "# ─── Render & Output ─────────────────────────────────────────", "", f'_uf_source = f\'\'\'page "{page_title}" {page_width}', "''' + \"\\n\".join(_uf_source_parts)", "", f"result = uframe.compile(_uf_source, width={page_width})", "", "if _cache_seconds >= 0:", ' print(f"#!c={_cache_seconds}")', "print(result.micron)", ] return "\n".join(lines) def compile_dynamic(page: Page) -> str: """Generate a self-contained executable Python script from an IR tree. The generated script imports uframe at runtime and compiles the dynamically-built .uf source into Micron output. """ logic_lines = _emit_node(page, indent_level=0) page_logic = "\n".join(logic_lines) uframe_import = "import uframe" return _build_script( uframe_import=uframe_import, page_logic=page_logic, page_title=page.title, page_width=page.width, )