feat: registry
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
"""µFrame compile endpoint — POST /api/compile."""
|
"""µFrame compile + DSL metadata endpoints."""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
import uframe
|
import uframe
|
||||||
|
import uframe.keywords # noqa: F401 — triggers keyword registration
|
||||||
from uframe.errors import UFrameError
|
from uframe.errors import UFrameError
|
||||||
|
from uframe.registry import get_dsl_meta
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -36,3 +38,9 @@ async def compile_source(req: CompileRequest):
|
|||||||
)
|
)
|
||||||
except UFrameError as e:
|
except UFrameError as e:
|
||||||
raise HTTPException(status_code=422, detail=str(e))
|
raise HTTPException(status_code=422, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dsl-meta")
|
||||||
|
async def dsl_meta():
|
||||||
|
"""Return DSL metadata for frontend syntax highlighting and autocomplete."""
|
||||||
|
return get_dsl_meta()
|
||||||
|
|||||||
249
backend/uframe/keywords.py
Normal file
249
backend/uframe/keywords.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
"""µFrame Keyword Registrations — single source of truth for all DSL keywords.
|
||||||
|
|
||||||
|
Each keyword is registered here with its metadata (section, detail, snippet,
|
||||||
|
highlight values). The actual parse/measure/layout/paint/codegen functions
|
||||||
|
remain in their respective modules for now — this file serves as the
|
||||||
|
registry that the frontend reads via GET /api/dsl-meta.
|
||||||
|
|
||||||
|
To add a new keyword:
|
||||||
|
1. Define its IR node in ir.py
|
||||||
|
2. Add a register_keyword() call here
|
||||||
|
3. Add parse logic in parser.py
|
||||||
|
4. Add measure/layout/paint logic in their respective files
|
||||||
|
5. That's it — syntax highlighting and slash commands auto-update from the registry
|
||||||
|
"""
|
||||||
|
|
||||||
|
from uframe.registry import register_keyword, ALL_THEME_NAMES
|
||||||
|
from uframe.ir import (
|
||||||
|
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,
|
||||||
|
ComponentDef, ComponentUse,
|
||||||
|
)
|
||||||
|
from uframe.themes import BUILTIN_THEMES
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layout
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("page", node_class=Page, section="Layout",
|
||||||
|
detail='page "Title" 64', snippet='page "${title}" ${width:64}',
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("box", node_class=Box, section="Layout",
|
||||||
|
detail='box light "Title"', snippet='box ${weight:light} "${title}"',
|
||||||
|
highlight_values=["light", "heavy", "double", "rounded"],
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("row", node_class=Row, section="Layout",
|
||||||
|
detail="row [gap]", snippet="row ${gap:2}",
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("col", node_class=Col, section="Layout",
|
||||||
|
detail="col [width]", snippet="col ${width}",
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("spacer", node_class=Spacer, section="Layout",
|
||||||
|
detail="spacer [lines]", snippet="spacer",
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("pad", node_class=Pad, section="Layout",
|
||||||
|
detail="pad t r b l", snippet="pad ${top:1} ${right:1} ${bottom:1} ${left:1}",
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Content
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("heading", node_class=Heading, section="Content",
|
||||||
|
detail='heading 1 "Text"', snippet='heading ${level:1} "${text}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("text", node_class=Text, section="Content",
|
||||||
|
detail='text "Content"', snippet='text "${content}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("label", node_class=Label, section="Content",
|
||||||
|
detail='label "Key" "Value"', snippet='label "${key}" "${value}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("divider", node_class=Divider, section="Content",
|
||||||
|
detail="divider heavy", snippet="divider ${style:light}",
|
||||||
|
highlight_values=["light", "heavy", "double", "dash", "dot"],
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("link", node_class=Link, section="Content",
|
||||||
|
detail='link "Text" "/path.mu"', snippet='link "${display}" "${dest}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("list", node_class=ListNode, section="Content",
|
||||||
|
detail="list bullet", snippet='list ${style:bullet}\n item "${entry}"',
|
||||||
|
highlight_values=["bullet", "dash", "number", "arrow"],
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("item", node_class=ListItem, section="",
|
||||||
|
detail="", snippet="", # not shown in slash commands (child of list)
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Data Visualization
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("gauge", node_class=Gauge, section="Data",
|
||||||
|
detail="gauge label val max width",
|
||||||
|
snippet='gauge "${label}" ${value} ${max:100} ${width:28} warn=${warn:75} crit=${crit:90}',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("sparkline", node_class=Sparkline, section="Data",
|
||||||
|
detail="sparkline label values width",
|
||||||
|
snippet='sparkline "${label}" "${values}" ${width:20}',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("status", node_class=Status, section="Data",
|
||||||
|
detail="status label state",
|
||||||
|
snippet='status "${label}" ${state:online}',
|
||||||
|
highlight_values=["online", "offline", "degraded", "unknown", "alert"],
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("table", node_class=Table, section="Data",
|
||||||
|
detail="table + columns + rows",
|
||||||
|
snippet='table "Title"\n columns "Name" 20 | "Value" 10\n row "entry" | "data"',
|
||||||
|
is_leaf=True) # table handles its own children (columns/row pseudo-nodes)
|
||||||
|
|
||||||
|
register_keyword("columns", section="",
|
||||||
|
detail="", snippet="") # child of table, not shown in slash commands
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Style
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("align", section="Style",
|
||||||
|
detail="align center", snippet="align ${align:center}",
|
||||||
|
highlight_values=["left", "center", "right"],
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
register_keyword("color", section="Style",
|
||||||
|
detail="color hex", snippet="color ${hex}",
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
register_keyword("bg", section="Style",
|
||||||
|
detail="", snippet="",
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
register_keyword("bold", section="Style",
|
||||||
|
detail="bold", snippet="bold",
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
register_keyword("italic", section="Style",
|
||||||
|
detail="", snippet="",
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
register_keyword("underline", section="Style",
|
||||||
|
detail="", snippet="",
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Forms
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("form", node_class=Form, section="Form",
|
||||||
|
detail='form "name"', snippet='form "${name}"',
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("field", node_class=Field, section="Form",
|
||||||
|
detail='field "name" 24 "placeholder"',
|
||||||
|
snippet='field "${name}" ${width:24} "${placeholder}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("password", node_class=Password, section="Form",
|
||||||
|
detail='password "name" 24 "placeholder"',
|
||||||
|
snippet='password "${name}" ${width:24} "${placeholder}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("radio", node_class=Radio, section="Form",
|
||||||
|
detail='radio "group" "A" | "B" | "C"',
|
||||||
|
snippet='radio "${group}" "${opt1}" | "${opt2}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("checkbox", node_class=Checkbox, section="Form",
|
||||||
|
detail='checkbox "name" "Label"',
|
||||||
|
snippet='checkbox "${name}" "${label}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
register_keyword("button", node_class=FormButton, section="Form",
|
||||||
|
detail='button "Label" "/action"',
|
||||||
|
snippet='button "${label}" "${dest}"',
|
||||||
|
is_leaf=True)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dynamic
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("let", node_class=Let, section="Dynamic",
|
||||||
|
detail='let name = "value"', snippet='let ${name} = "${value}"',
|
||||||
|
is_metadata=True)
|
||||||
|
|
||||||
|
register_keyword("source", node_class=Source, section="Dynamic",
|
||||||
|
detail='source name : shell "cmd"',
|
||||||
|
snippet='source ${name} : shell "${command}"',
|
||||||
|
highlight_values=["shell", "file", "json", "python", "rns", "param"],
|
||||||
|
is_metadata=True)
|
||||||
|
|
||||||
|
register_keyword("if", node_class=IfBlock, section="Dynamic",
|
||||||
|
detail="if $var > threshold", snippet="if ${condition}",
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("elif", section="Dynamic",
|
||||||
|
detail="", snippet="")
|
||||||
|
|
||||||
|
register_keyword("else", section="Dynamic",
|
||||||
|
detail="", snippet="")
|
||||||
|
|
||||||
|
register_keyword("for", node_class=ForLoop, section="Dynamic",
|
||||||
|
detail="for item in $list", snippet='for ${var} in ${iterable}',
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("cache", node_class=CacheControl, section="Dynamic",
|
||||||
|
detail="cache 0", snippet="cache ${seconds:0}",
|
||||||
|
is_metadata=True)
|
||||||
|
|
||||||
|
register_keyword("on_submit", node_class=OnSubmit, section="Dynamic",
|
||||||
|
detail='on_submit "form"', snippet='on_submit "${form_name}"',
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("state", node_class=StateDecl, section="Dynamic",
|
||||||
|
detail='state "name" "/path.json"',
|
||||||
|
snippet='state "${name}" "${path}"',
|
||||||
|
is_metadata=True)
|
||||||
|
|
||||||
|
register_keyword("set", section="Dynamic", detail="", snippet="")
|
||||||
|
register_keyword("append", section="Dynamic", detail="", snippet="")
|
||||||
|
register_keyword("prepend", section="Dynamic", detail="", snippet="")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Themes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("theme", section="Theme",
|
||||||
|
detail="", snippet="",
|
||||||
|
is_style_directive=True)
|
||||||
|
|
||||||
|
# Register each built-in theme as a named entry
|
||||||
|
for _theme_name, _theme_def in BUILTIN_THEMES.items():
|
||||||
|
register_keyword(f"theme_{_theme_name}", section="Theme",
|
||||||
|
detail=f"{_theme_def.description}",
|
||||||
|
snippet=f"theme {_theme_name}")
|
||||||
|
ALL_THEME_NAMES.append(_theme_name)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Components
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
register_keyword("component", node_class=ComponentDef, section="",
|
||||||
|
detail="", snippet="",
|
||||||
|
is_container=True)
|
||||||
|
|
||||||
|
register_keyword("use", section="",
|
||||||
|
detail="", snippet="")
|
||||||
130
backend/uframe/registry.py
Normal file
130
backend/uframe/registry.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""µ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,
|
||||||
|
}
|
||||||
@@ -224,15 +224,39 @@ const COMMANDS: CmdEntry[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Dynamic commands from /api/dsl-meta — merged into COMMANDS
|
||||||
|
let dynamicCommands: CmdEntry[] = [];
|
||||||
|
|
||||||
|
/** Update slash commands from /api/dsl-meta response. */
|
||||||
|
export function setDslCommands(
|
||||||
|
commands: { label: string; detail: string; section: string; snippet: string }[],
|
||||||
|
) {
|
||||||
|
// Build dynamic commands from API data, only for entries not already in COMMANDS
|
||||||
|
const existingLabels = new Set(COMMANDS.map((c) => c.label));
|
||||||
|
dynamicCommands = commands
|
||||||
|
.filter((c) => c.detail && c.snippet && !existingLabels.has(c.label))
|
||||||
|
.map((c) => ({
|
||||||
|
label: c.label,
|
||||||
|
detail: c.detail,
|
||||||
|
section: c.section,
|
||||||
|
apply: c.snippet.includes("${")
|
||||||
|
? slashSnippet(c.snippet)
|
||||||
|
: insert(c.snippet),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export function uframeCommandSource(
|
export function uframeCommandSource(
|
||||||
ctx: CompletionContext,
|
ctx: CompletionContext,
|
||||||
): CompletionResult | null {
|
): CompletionResult | null {
|
||||||
const match = ctx.matchBefore(/\/\w*/);
|
const match = ctx.matchBefore(/\/\w*/);
|
||||||
if (!match || (match.from === match.to && !ctx.explicit)) return null;
|
if (!match || (match.from === match.to && !ctx.explicit)) return null;
|
||||||
|
|
||||||
|
const allCommands = [...COMMANDS, ...dynamicCommands];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: match.from + 1,
|
from: match.from + 1,
|
||||||
filter: true,
|
filter: true,
|
||||||
options: COMMANDS.map((cmd) => ({
|
options: allCommands.map((cmd) => ({
|
||||||
label: cmd.label,
|
label: cmd.label,
|
||||||
detail: cmd.detail,
|
detail: cmd.detail,
|
||||||
section: cmd.section,
|
section: cmd.section,
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import { tags } from "@lezer/highlight";
|
|||||||
/**
|
/**
|
||||||
* CodeMirror 6 syntax highlighting for the µFrame .uf DSL.
|
* CodeMirror 6 syntax highlighting for the µFrame .uf DSL.
|
||||||
*
|
*
|
||||||
* Keywords: page, box, row, col, spacer, pad, heading, text, label,
|
* Keywords and values can be updated dynamically via setDslKeywords()
|
||||||
* divider, link, list, item, gauge, sparkline, status,
|
* which is called when the frontend fetches /api/dsl-meta.
|
||||||
* table, columns, form, field, radio, checkbox, button,
|
|
||||||
* source, let, if, elif, else, for, align, color, bg,
|
|
||||||
* bold, italic, underline, cache, state, on_submit
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const KEYWORDS = new Set([
|
// Mutable sets — updated from /api/dsl-meta
|
||||||
|
let KEYWORDS = new Set([
|
||||||
|
// Fallback defaults (used before API response arrives)
|
||||||
"page", "box", "row", "col", "spacer", "pad",
|
"page", "box", "row", "col", "spacer", "pad",
|
||||||
"heading", "text", "label", "divider", "link",
|
"heading", "text", "label", "divider", "link",
|
||||||
"list", "item", "gauge", "sparkline", "status",
|
"list", "item", "gauge", "sparkline", "status",
|
||||||
@@ -23,17 +22,24 @@ const KEYWORDS = new Set([
|
|||||||
"checkbox", "button", "source", "let", "if",
|
"checkbox", "button", "source", "let", "if",
|
||||||
"elif", "else", "for", "align", "color", "bg",
|
"elif", "else", "for", "align", "color", "bg",
|
||||||
"bold", "italic", "underline", "cache", "state",
|
"bold", "italic", "underline", "cache", "state",
|
||||||
"on_submit", "meter", "bar_h", "bar_v", "bar",
|
"on_submit", "theme", "component", "use",
|
||||||
"heatmap", "component", "use",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const WEIGHT_VALS = new Set([
|
let WEIGHT_VALS = new Set([
|
||||||
"light", "heavy", "double", "rounded",
|
"light", "heavy", "double", "rounded",
|
||||||
"bullet", "dash", "number", "arrow",
|
"bullet", "dash", "number", "arrow",
|
||||||
"left", "center", "right",
|
"left", "center", "right",
|
||||||
"online", "offline", "degraded", "unknown",
|
"online", "offline", "degraded", "unknown",
|
||||||
|
"shell", "file", "json", "python", "rns", "param",
|
||||||
|
"default", "nouveau", "gothic", "bamboo", "circuit", "brutalist",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** Update keywords and values from /api/dsl-meta response. */
|
||||||
|
export function setDslKeywords(keywords: string[], values: string[]) {
|
||||||
|
if (keywords.length > 0) KEYWORDS = new Set(keywords);
|
||||||
|
if (values.length > 0) WEIGHT_VALS = new Set(values);
|
||||||
|
}
|
||||||
|
|
||||||
const uframeLanguage = StreamLanguage.define({
|
const uframeLanguage = StreamLanguage.define({
|
||||||
token(stream) {
|
token(stream) {
|
||||||
// Comments
|
// Comments
|
||||||
|
|||||||
35
frontend/src/hooks/useDslMeta.ts
Normal file
35
frontend/src/hooks/useDslMeta.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export interface DslMeta {
|
||||||
|
keywords: string[];
|
||||||
|
values: string[];
|
||||||
|
commands: { label: string; detail: string; section: string; snippet: string }[];
|
||||||
|
themes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_META: DslMeta = {
|
||||||
|
keywords: [],
|
||||||
|
values: [],
|
||||||
|
commands: [],
|
||||||
|
themes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedMeta: DslMeta | null = null;
|
||||||
|
|
||||||
|
export function useDslMeta(): DslMeta {
|
||||||
|
const [meta, setMeta] = useState<DslMeta>(cachedMeta || DEFAULT_META);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cachedMeta) return;
|
||||||
|
|
||||||
|
fetch("/api/dsl-meta")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data: DslMeta) => {
|
||||||
|
cachedMeta = data;
|
||||||
|
setMeta(data);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
@@ -6,8 +6,9 @@ import { useEditorStore } from "@/stores/editorStore";
|
|||||||
import { usePagesStore } from "@/stores/pagesStore";
|
import { usePagesStore } from "@/stores/pagesStore";
|
||||||
import { useUnsavedGuard } from "@/hooks/useUnsavedGuard";
|
import { useUnsavedGuard } from "@/hooks/useUnsavedGuard";
|
||||||
import { useCompile } from "@/hooks/useCompile";
|
import { useCompile } from "@/hooks/useCompile";
|
||||||
import { uframeHighlight } from "@/components/editor/uframeHighlight";
|
import { useDslMeta } from "@/hooks/useDslMeta";
|
||||||
import { uframeCommandSource } from "@/components/editor/uframeCommands";
|
import { uframeHighlight, setDslKeywords } from "@/components/editor/uframeHighlight";
|
||||||
|
import { uframeCommandSource, setDslCommands } from "@/components/editor/uframeCommands";
|
||||||
import EditorPane from "@/components/editor/EditorPane";
|
import EditorPane from "@/components/editor/EditorPane";
|
||||||
import PreviewPane from "@/components/editor/PreviewPane";
|
import PreviewPane from "@/components/editor/PreviewPane";
|
||||||
import ToolBar from "@/components/editor/ToolBar";
|
import ToolBar from "@/components/editor/ToolBar";
|
||||||
@@ -47,6 +48,15 @@ export default function EditorView() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Load DSL metadata (keywords, values, commands) from backend
|
||||||
|
const dslMeta = useDslMeta();
|
||||||
|
useEffect(() => {
|
||||||
|
if (dslMeta.keywords.length > 0) {
|
||||||
|
setDslKeywords(dslMeta.keywords, dslMeta.values);
|
||||||
|
setDslCommands(dslMeta.commands);
|
||||||
|
}
|
||||||
|
}, [dslMeta]);
|
||||||
|
|
||||||
// Auto-compile on source changes
|
// Auto-compile on source changes
|
||||||
useCompile();
|
useCompile();
|
||||||
useUnsavedGuard();
|
useUnsavedGuard();
|
||||||
|
|||||||
Reference in New Issue
Block a user