131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
"""µ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,
|
|
}
|