feat: dynamic mode

This commit is contained in:
2026-04-01 08:28:44 +02:00
parent df11705875
commit 619d3ec538
16 changed files with 1184 additions and 38 deletions

View File

@@ -18,6 +18,9 @@ from uframe.ir import (
IRNode, Page, Box, Row, Col, Spacer, Pad,
Heading, Text, Label, Divider, Link, ListNode, ListItem,
Gauge, Sparkline, Status, Table, TextSpan,
Form, Field, Password, Radio, Checkbox, FormButton,
Let, Source, IfBlock, ForLoop, CacheControl, OnSubmit, StateDecl,
SourceType,
BorderWeight, HeadingLevel, DividerStyle, ListStyle, Align, Style,
)
@@ -245,9 +248,19 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
# Phase 4 placeholders
elif keyword == "gauge":
label = args[0] if args else ""
value = float(args[1]) if len(args) > 1 else 0
max_val = float(args[2]) if len(args) > 2 else 100
bar_width = int(args[3]) if len(args) > 3 else 28
val_str = args[1] if len(args) > 1 else "0"
try:
value = float(val_str)
except ValueError:
value = 0 # $variable — resolved at runtime
try:
max_val = float(args[2]) if len(args) > 2 else 100
except ValueError:
max_val = 100
try:
bar_width = int(args[3]) if len(args) > 3 else 28
except ValueError:
bar_width = 28
# Parse warn=N crit=N from remaining args
warn = crit = None
for a in args[4:]:
@@ -289,10 +302,139 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode:
cols.append((col_name, col_w))
return _TableColumns(cols, line_num)
# Forms
elif keyword == "form":
form_name = args[0] if args else ""
return Form(form_name=form_name, source_line=line_num)
elif keyword == "field":
name = args[0] if args else ""
width = int(args[1]) if len(args) > 1 else 24
placeholder = args[2] if len(args) > 2 else ""
return Field(field_name=name, field_width=width, placeholder=placeholder,
source_line=line_num)
elif keyword == "password":
name = args[0] if args else ""
width = int(args[1]) if len(args) > 1 else 24
placeholder = args[2] if len(args) > 2 else ""
return Password(field_name=name, field_width=width, placeholder=placeholder,
source_line=line_num)
elif keyword == "radio":
group = args[0] if args else ""
raw = " ".join(args[1:]) if len(args) > 1 else ""
options = [o.strip().strip('"') for o in raw.split("|")] if raw else []
return Radio(group=group, options=options, source_line=line_num)
elif keyword == "checkbox":
name = args[0] if args else ""
label_text = args[1] if len(args) > 1 else ""
return Checkbox(field_name=name, checkbox_label=label_text, source_line=line_num)
elif keyword == "button":
label_text = args[0] if args else ""
dest = args[1] if len(args) > 1 else ""
return FormButton(button_label=label_text, dest=dest, source_line=line_num)
# Dynamic features
elif keyword == "let":
# let name = "value" or let name = 1,2,3
raw = " ".join(args)
eq = raw.find("=")
if eq != -1:
var_name = raw[:eq].strip()
var_value = raw[eq + 1:].strip().strip('"')
else:
var_name = args[0] if args else ""
var_value = args[1] if len(args) > 1 else ""
return Let(var_name=var_name, var_value=var_value, source_line=line_num)
elif keyword == "source":
# source cpu : shell "grep 'cpu' /proc/stat"
# source name : type "command"
raw = " ".join(args)
colon = raw.find(":")
if colon != -1:
var_name = raw[:colon].strip()
rest = raw[colon + 1:].strip()
parts = _split_args(rest)
src_type_str = parts[0] if parts else "shell"
command = parts[1] if len(parts) > 1 else ""
src_type = {
"shell": SourceType.SHELL,
"file": SourceType.FILE,
"json": SourceType.JSON,
"python": SourceType.PYTHON,
"rns": SourceType.RNS,
"param": SourceType.PARAM,
}.get(src_type_str.lower(), SourceType.SHELL)
# Parse optional timeout
timeout = 5
for p in parts[2:]:
if p.startswith("timeout"):
try:
timeout = int(p.split("=")[1]) if "=" in p else int(parts[parts.index(p) + 1])
except (ValueError, IndexError):
pass
return Source(var_name=var_name, source_type=src_type,
command=command, timeout=timeout, source_line=line_num)
else:
return Source(var_name=args[0] if args else "", source_line=line_num)
elif keyword == "if":
condition = " ".join(args)
return IfBlock(condition=condition, source_line=line_num)
elif keyword == "elif":
condition = " ".join(args)
return _ElifBranch(condition, line_num)
elif keyword == "else":
return _ElseBranch(line_num)
elif keyword == "for":
# for item in $collection
var_name = args[0] if args else "item"
# Skip "in" keyword
iterable = args[2] if len(args) > 2 else (args[1] if len(args) > 1 else "")
return ForLoop(var_name=var_name, iterable=iterable, source_line=line_num)
elif keyword == "cache":
seconds = int(args[0]) if args else 0
return CacheControl(seconds=seconds, source_line=line_num)
elif keyword == "on_submit":
form_name = args[0] if args else ""
return OnSubmit(form_name=form_name, source_line=line_num)
elif keyword == "state":
state_name = args[0] if args else ""
path = args[1] if len(args) > 1 else ""
return StateDecl(state_name=state_name, path=path, source_line=line_num)
elif keyword in ("set", "append", "prepend"):
# State operations — store as text nodes with metadata for the compiler
content = " ".join([keyword] + args)
return Text(content=content, source_line=line_num)
else:
raise ParseError(f"Unknown keyword: {keyword!r}", line=line_num)
class _ElifBranch(IRNode):
"""Temporary node — absorbed by parent IfBlock during tree building."""
def __init__(self, condition: str, line_num: int):
super().__init__(source_line=line_num)
self.condition = condition
class _ElseBranch(IRNode):
"""Temporary node — absorbed by parent IfBlock during tree building."""
def __init__(self, line_num: int):
super().__init__(source_line=line_num)
class _TableColumns(IRNode):
"""Temporary node — absorbed by parent Table during tree building."""
def __init__(self, columns: list[tuple[str, int]], line_num: int):
@@ -377,6 +519,24 @@ def parse(source: str) -> Page:
stack[-1][1].rows.append(node.cells)
continue
# elif/else branches are absorbed by the nearest IfBlock ancestor
if isinstance(node, _ElifBranch):
# Find the IfBlock in the stack
for si in range(len(stack) - 1, -1, -1):
if isinstance(stack[si][1], IfBlock):
# Collect subsequent children under this elif
stack[si][1].elif_branches.append((node.condition, []))
break
continue
if isinstance(node, _ElseBranch):
# Find the IfBlock in the stack — mark it for else collection
for si in range(len(stack) - 1, -1, -1):
if isinstance(stack[si][1], IfBlock):
stack[si][1].else_children = [] # will be filled by subsequent children
break
continue
# Attach to parent
if stack:
parent = stack[-1][1]