""" core/validator.py Two validators: validate(spec) — legacy single-component spec (unchanged) validate_page_spec(spec) — new page spec """ from __future__ import annotations import logging import re logger = logging.getLogger(__name__) SUPPORTED_COMPONENTS = { "Slider", "Textbox", "Image", "Dropdown", "Checkbox", "Button", "Audio", "File", "Plot", "Dataframe", "Number", "HTML", "Radio", } FALLBACK_SPEC = { "component": "Textbox", "props": {"label": "Output", "placeholder": "Result will appear here"}, "layout": "vertical", "connected_output": None, } ALLOWED_PROPS: dict[str, set[str]] = { "Slider": {"label", "minimum", "maximum", "step", "value", "info"}, "Textbox": {"label", "placeholder", "value", "lines", "max_lines", "info"}, "Image": {"label", "type", "height", "width", "info"}, "Dropdown": {"label", "choices", "value", "multiselect", "info"}, "Checkbox": {"label", "value", "info"}, "Button": {"value", "variant", "size", "icon"}, "Audio": {"label", "type", "format", "info"}, "File": {"label", "file_count", "file_types", "info"}, "Plot": {"label", "format"}, "Dataframe": {"label", "headers", "datatype", "row_count", "col_count", "info"}, "Number": {"label", "value", "minimum", "maximum", "step", "info"}, "HTML": {"html_template", "css"}, "Radio": {"label", "choices", "value", "info"}, } _CSS_FORBIDDEN = re.compile(r"url\s*\(|@import|fetch\s*\(| tuple[bool, dict]: """Validate a single-component spec dict. Returns (ok, validated_spec_or_fallback).""" if not isinstance(spec, dict): logger.warning("spec is not a dict — using fallback") return False, FALLBACK_SPEC.copy() component = spec.get("component", "") if component not in SUPPORTED_COMPONENTS: logger.warning("Unknown component %r — using fallback", component) return False, FALLBACK_SPEC.copy() if "props" not in spec or not isinstance(spec.get("props"), dict): logger.warning("Missing or invalid props for %r — using fallback", component) return False, FALLBACK_SPEC.copy() allowed = ALLOWED_PROPS.get(component, set()) filtered = {k: v for k, v in spec["props"].items() if k in allowed} clean_spec = { "component": component, "props": filtered, "layout": spec.get("layout", "vertical"), "connected_output": spec.get("connected_output", None), } return True, clean_spec _VALID_LAYOUTS = {"centered", "sidebar", "dashboard", "split", "fullpage", "tabbed"} _VALID_SECTION_TYPES = {"hero", "form", "result", "chart", "table", "gallery", "nav", "footer", "custom"} _VALID_ROLES = {"input", "output", "control", "display"} _VALID_ACTIONS = {"new", "edit", "add", "remove", "replace", "wire", "restyle"} def validate_page_spec(spec: dict) -> tuple[bool, list[str]]: """ Validate a page spec. Returns (ok, errors) where errors is a list of human-readable strings. ok is True only when errors is empty. Checks: - page.sections is a non-empty list - every section has id, type, components list - every component has id, component, role - no duplicate ids across all sections and all components - every glue_fn trigger references a real section+component id - every glue_fn output references a real section+component id - html_shell is non-empty for non-custom sections - CSS fields do not contain url(), @import, fetch(), or