feat: dynamic mode
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user