"""µFrame Keyword Registry — single source of truth for all DSL keywords. Each keyword is registered with its parse, measure, layout, paint, and codegen functions plus frontend metadata (section, detail, snippet). Adding a new keyword requires only one registration in keywords.py. Usage: from uframe.registry import register, KEYWORD_REGISTRY, NODE_REGISTRY @register("gauge", section="Data", detail="gauge label val max width", snippet='gauge "${label}" ${value} ${max:100} ${width:28}') def _def_gauge(): return KeywordDef( parse=parse_gauge, measure=measure_gauge, paint=paint_gauge, ... ) """ from __future__ import annotations from dataclasses import dataclass, field from typing import Callable, Any from uframe.ir import IRNode @dataclass class KeywordDef: """Complete definition of a µFrame DSL keyword.""" name: str = "" section: str = "" # "Layout", "Content", "Data", "Style", "Theme", "Form", "Dynamic" detail: str = "" # slash command detail text snippet: str = "" # slash command snippet (CodeMirror format) highlight_values: list[str] = field(default_factory=list) # values to highlight as atoms # Pipeline functions — all optional, falling back to generic behavior parse: Callable[..., IRNode] | None = None # (args, line_num) → IRNode measure: Callable[..., None] | None = None # (node, available_width) → None layout: Callable[..., int] | None = None # (node, x, y, w, h) → height paint: Callable[..., None] | None = None # (node, grid, theme) → None codegen: Callable[..., list[str]] | None = None # (node, indent_level) → [str] # Flags is_container: bool = False # has children (affects layout: vertical stack) is_leaf: bool = False # simple leaf node (layout: return pref_height) is_metadata: bool = False # zero-height metadata (let, source, cache) is_style_directive: bool = False # modifies parent's style (align, color, bold) # --------------------------------------------------------------------------- # Registries # --------------------------------------------------------------------------- # Keyword name → KeywordDef KEYWORD_REGISTRY: dict[str, KeywordDef] = {} # IR node class → KeywordDef (for measure/layout/paint/codegen dispatch) NODE_REGISTRY: dict[type, KeywordDef] = {} # All highlight values (populated during registration) ALL_HIGHLIGHT_VALUES: set[str] = set() # All theme names ALL_THEME_NAMES: list[str] = [] def register(name: str, node_class: type | None = None, **kwargs: Any) -> Callable: """Decorator to register a keyword definition. Usage: @register("gauge", node_class=Gauge, section="Data", detail="gauge label val max width", snippet='gauge "${label}" ${value} ...') def def_gauge(): return KeywordDef(parse=..., measure=..., paint=..., ...) Or simpler — pass all fields directly: register_keyword("gauge", node_class=Gauge, section="Data", ...) """ def decorator(func: Callable) -> Callable: kw_def = func() if isinstance(kw_def, KeywordDef): kw_def.name = name for k, v in kwargs.items(): if hasattr(kw_def, k): setattr(kw_def, k, v) else: kw_def = KeywordDef(name=name, **kwargs) KEYWORD_REGISTRY[name] = kw_def if node_class is not None: NODE_REGISTRY[node_class] = kw_def ALL_HIGHLIGHT_VALUES.update(kw_def.highlight_values) return func return decorator def register_keyword(name: str, node_class: type | None = None, **kwargs: Any) -> KeywordDef: """Direct registration (non-decorator form).""" kw_def = KeywordDef(name=name, **kwargs) KEYWORD_REGISTRY[name] = kw_def if node_class is not None: NODE_REGISTRY[node_class] = kw_def ALL_HIGHLIGHT_VALUES.update(kw_def.highlight_values) return kw_def def get_dsl_meta() -> dict: """Return DSL metadata for the frontend (keywords, values, commands, themes).""" keywords = sorted(KEYWORD_REGISTRY.keys()) values = sorted(ALL_HIGHLIGHT_VALUES) commands = [] for kw in KEYWORD_REGISTRY.values(): if kw.detail and kw.snippet: commands.append({ "label": kw.name, "detail": kw.detail, "section": kw.section, "snippet": kw.snippet, }) themes = ALL_THEME_NAMES or [] return { "keywords": keywords, "values": values, "commands": commands, "themes": themes, }