From a8bfff9e0b5d5636fae33e50ae0e3b74df3e86a8 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 8 Mar 2026 00:00:23 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Remove=20CSSX=20style=20dictionary=20infras?= =?UTF-8?q?tructure=20=E2=80=94=20styling=20is=20just=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entire parallel CSS system (StyleValue type, style dictionary, keyword atom resolver, content-addressed class generation, runtime CSS injection, localStorage caching) was built but never adopted — the codebase already uses :class strings with defcomp components for all styling. Remove ~3,000 lines of unused infrastructure. Deleted: - cssx.sx spec module (317 lines) - style_dict.py (782 lines) and style_resolver.py (254 lines) - StyleValue type, defkeyframes special form, build-keyframes platform fn - Style dict JSON delivery ( @@ -820,14 +819,6 @@ def sx_page(ctx: dict, page_sx: str, *, import logging logging.getLogger("sx").warning("Pretty-print page_sx failed: %s", e) - # Style dictionary for client-side css primitive - styles_hash = _get_style_dict_hash() - client_styles_hash = _get_sx_styles_cookie() - if not _is_dev_mode() and client_styles_hash and client_styles_hash == styles_hash: - styles_json = "" # Client has cached version - else: - styles_json = _build_style_dict_json() - # Page registry for client-side routing import logging _plog = logging.getLogger("sx.pages") @@ -844,8 +835,6 @@ def sx_page(ctx: dict, page_sx: str, *, csrf=_html_escape(csrf), component_hash=component_hash, component_defs=component_defs, - styles_hash=styles_hash, - styles_json=styles_json, pages_sx=pages_sx, page_sx=page_sx, sx_css=sx_css, @@ -907,10 +896,6 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *, title = ctx.get("base_title", "Rose Ash") csrf = _get_csrf_token() - styles_hash = _get_style_dict_hash() - client_styles_hash = _get_sx_styles_cookie() - styles_json = "" if (not _is_dev_mode() and client_styles_hash == styles_hash) else _build_style_dict_json() - import logging from quart import current_app pages_sx = _build_pages_sx(current_app.name) @@ -953,7 +938,6 @@ def sx_page_streaming_parts(ctx: dict, page_html: str, *, '\n' '\n' '\n' - f'\n' f'\n' f'\n' # Server-rendered HTML — suspense placeholders are real DOM elements @@ -989,58 +973,6 @@ def sx_streaming_resolve_script(suspension_id: str, sx_source: str, _SCRIPT_HASH_CACHE: dict[str, str] = {} -_STYLE_DICT_JSON: str = "" -_STYLE_DICT_HASH: str = "" - - -def _build_style_dict_json() -> str: - """Build compact JSON style dictionary for client-side css primitive.""" - global _STYLE_DICT_JSON, _STYLE_DICT_HASH - if _STYLE_DICT_JSON: - return _STYLE_DICT_JSON - - import json - from .style_dict import ( - STYLE_ATOMS, PSEUDO_VARIANTS, RESPONSIVE_BREAKPOINTS, - KEYFRAMES, ARBITRARY_PATTERNS, CHILD_SELECTOR_ATOMS, - ) - - # Derive child selector prefixes from CHILD_SELECTOR_ATOMS - prefixes = set() - for atom in CHILD_SELECTOR_ATOMS: - # "space-y-4" → "space-y-", "divide-y" → "divide-" - for sep in ("space-x-", "space-y-", "divide-x", "divide-y"): - if atom.startswith(sep): - prefixes.add(sep) - break - - data = { - "a": STYLE_ATOMS, - "v": PSEUDO_VARIANTS, - "b": RESPONSIVE_BREAKPOINTS, - "k": KEYFRAMES, - "p": ARBITRARY_PATTERNS, - "c": sorted(prefixes), - } - _STYLE_DICT_JSON = json.dumps(data, separators=(",", ":")) - _STYLE_DICT_HASH = hashlib.md5(_STYLE_DICT_JSON.encode()).hexdigest()[:8] - return _STYLE_DICT_JSON - - -def _get_style_dict_hash() -> str: - """Get the hash of the style dictionary JSON.""" - if not _STYLE_DICT_HASH: - _build_style_dict_json() - return _STYLE_DICT_HASH - - -def _get_sx_styles_cookie() -> str: - """Read the sx-styles-hash cookie from the current request.""" - try: - from quart import request - return request.cookies.get("sx-styles-hash", "") - except RuntimeError: - return "" def _script_hash(filename: str) -> str: diff --git a/shared/sx/html.py b/shared/sx/html.py index 8123ada..820f341 100644 --- a/shared/sx/html.py +++ b/shared/sx/html.py @@ -27,7 +27,7 @@ from __future__ import annotations import contextvars from typing import Any -from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol +from .types import Component, Keyword, Lambda, Macro, NIL, Symbol from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline def _eval(expr, env): @@ -510,19 +510,6 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str: children.append(arg) i += 1 - # Handle :style StyleValue — convert to class and register CSS rule - style_val = attrs.get("style") - if isinstance(style_val, StyleValue): - from .css_registry import register_generated_rule - register_generated_rule(style_val) - # Merge into :class - existing_class = attrs.get("class") - if existing_class and existing_class is not NIL and existing_class is not False: - attrs["class"] = f"{existing_class} {style_val.class_name}" - else: - attrs["class"] = style_val.class_name - del attrs["style"] - # Collect CSS classes if collector is active class_val = attrs.get("class") if class_val is not None and class_val is not NIL and class_val is not False: diff --git a/shared/sx/parser.py b/shared/sx/parser.py index 4017ec4..f38af53 100644 --- a/shared/sx/parser.py +++ b/shared/sx/parser.py @@ -380,11 +380,6 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str: items.append(serialize(v, indent, pretty)) return "{" + " ".join(items) + "}" - # StyleValue — serialize as class name string - from .types import StyleValue - if isinstance(expr, StyleValue): - return f'"{expr.class_name}"' - # _RawHTML — pre-rendered HTML; wrap as (raw! "...") for SX wire format from .html import _RawHTML if isinstance(expr, _RawHTML): diff --git a/shared/sx/primitives_stdlib.py b/shared/sx/primitives_stdlib.py index d019339..650aa12 100644 --- a/shared/sx/primitives_stdlib.py +++ b/shared/sx/primitives_stdlib.py @@ -89,37 +89,6 @@ def prim_strip_tags(s: str) -> str: return re.sub(r"<[^>]+>", "", s) -# --------------------------------------------------------------------------- -# stdlib.style -# --------------------------------------------------------------------------- - -@register_primitive("css") -def prim_css(*args: Any) -> Any: - """``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.""" - from .types import Keyword - from .style_resolver import resolve_style - atoms = tuple( - (a.name if isinstance(a, Keyword) else str(a)) - for a in args if a is not None and a is not NIL and a is not False - ) - if not atoms: - return NIL - return resolve_style(atoms) - - -@register_primitive("merge-styles") -def prim_merge_styles(*styles: Any) -> Any: - """``(merge-styles style1 style2)`` → merged StyleValue.""" - from .types import StyleValue - from .style_resolver import merge_styles - valid = [s for s in styles if isinstance(s, StyleValue)] - if not valid: - return NIL - if len(valid) == 1: - return valid[0] - return merge_styles(valid) - - # --------------------------------------------------------------------------- # stdlib.debug # --------------------------------------------------------------------------- diff --git a/shared/sx/ref/BOUNDARY.md b/shared/sx/ref/BOUNDARY.md index 40b4279..8b151c9 100644 --- a/shared/sx/ref/BOUNDARY.md +++ b/shared/sx/ref/BOUNDARY.md @@ -38,7 +38,6 @@ Only these types may cross the host-SX boundary: | list | `list` | `Array` | `Vec` | | dict | `dict` | `Object` / `Map` | `HashMap` | | sx-source | `SxExpr` wrapper | `string` | `String` | -| style-value | `StyleValue` | `StyleValue` | `StyleValue` | **NOT allowed:** ORM models, datetime objects, request objects, raw callables, framework types. Convert at the edge before crossing. diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index fda72ee..f338a49 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -52,9 +52,6 @@ (create-fragment) (render-dom-list expr env ns)) - ;; Style value → text of class name - "style-value" (create-text-node (style-value-class expr)) - ;; Fallback :else (create-text-node (str expr))))) @@ -147,8 +144,7 @@ (let ((new-ns (cond (= tag "svg") SVG_NS (= tag "math") MATH_NS :else ns)) - (el (dom-create-element tag new-ns)) - (extra-class nil)) + (el (dom-create-element tag new-ns))) ;; Process args: keywords → attrs, others → children (reduce @@ -168,9 +164,6 @@ ;; nil or false → skip (or (nil? attr-val) (= attr-val false)) nil - ;; :style StyleValue → convert to class - (and (= attr-name "style") (style-value? attr-val)) - (set! extra-class (style-value-class attr-val)) ;; Boolean attr (contains? BOOLEAN_ATTRS attr-name) (when attr-val (dom-set-attr el attr-name "")) @@ -190,12 +183,6 @@ (dict "i" 0 "skip" false) args) - ;; Merge StyleValue class - (when extra-class - (let ((existing (dom-get-attr el "class"))) - (dom-set-attr el "class" - (if existing (str existing " " extra-class) extra-class)))) - el))) @@ -297,7 +284,7 @@ (define RENDER_DOM_FORMS (list "if" "when" "cond" "case" "let" "let*" "begin" "do" - "define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler" + "define" "defcomp" "defmacro" "defstyle" "defhandler" "map" "map-indexed" "filter" "for-each")) (define render-dom-form? @@ -450,7 +437,6 @@ ;; ;; From render.sx: ;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form? -;; style-value?, style-value-class ;; ;; From eval.sx: ;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx index cf9e702..a910035 100644 --- a/shared/sx/ref/adapter-html.sx +++ b/shared/sx/ref/adapter-html.sx @@ -41,7 +41,6 @@ "boolean" (if val "true" "false") "list" (render-list-to-html val env) "raw-html" (raw-html-content val) - "style-value" (style-value-class val) :else (escape-html (str val))))) @@ -51,7 +50,7 @@ (define RENDER_HTML_FORMS (list "if" "when" "cond" "case" "let" "let*" "begin" "do" - "define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler" + "define" "defcomp" "defmacro" "defstyle" "defhandler" "map" "map-indexed" "filter" "for-each")) (define render-html-form? @@ -293,7 +292,7 @@ ;; -------------------------------------------------------------------------- ;; ;; Inherited from render.sx: -;; escape-html, escape-attr, raw-html-content, style-value?, style-value-class +;; escape-html, escape-attr, raw-html-content ;; ;; From eval.sx: ;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond diff --git a/shared/sx/ref/async_eval_ref.py b/shared/sx/ref/async_eval_ref.py index e037e02..af9c903 100644 --- a/shared/sx/ref/async_eval_ref.py +++ b/shared/sx/ref/async_eval_ref.py @@ -26,7 +26,7 @@ import contextvars import inspect from typing import Any -from ..types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol +from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol from ..parser import SxExpr, serialize from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io from ..html import ( @@ -250,18 +250,6 @@ async def _arender_element(tag, args, env, ctx): children.append(arg) i += 1 - # StyleValue → class - style_val = attrs.get("style") - if isinstance(style_val, StyleValue): - from ..css_registry import register_generated_rule - register_generated_rule(style_val) - existing = attrs.get("class") - if existing and existing is not NIL and existing is not False: - attrs["class"] = f"{existing} {style_val.class_name}" - else: - attrs["class"] = style_val.class_name - del attrs["style"] - class_val = attrs.get("class") if class_val is not None and class_val is not NIL and class_val is not False: collector = css_class_collector.get(None) @@ -464,7 +452,6 @@ _ASYNC_RENDER_FORMS = { "do": _arsf_begin, "define": _arsf_define, "defstyle": _arsf_define, - "defkeyframes": _arsf_define, "defcomp": _arsf_define, "defmacro": _arsf_define, "defhandler": _arsf_define, @@ -716,23 +703,18 @@ async def _aser_call(name, args, env, ctx): if isinstance(arg, Keyword) and i + 1 < len(args): val = await _aser(args[i + 1], env, ctx) if val is not NIL and val is not None: - if arg.name == "style" and isinstance(val, StyleValue): - from ..css_registry import register_generated_rule - register_generated_rule(val) - extra_class = val.class_name - else: - parts.append(f":{arg.name}") - if isinstance(val, list): - live = [v for v in val if v is not NIL and v is not None] - items = [serialize(v) for v in live] - if not items: - parts.append("nil") - elif any(isinstance(v, SxExpr) for v in live): - parts.append("(<> " + " ".join(items) + ")") - else: - parts.append("(list " + " ".join(items) + ")") + parts.append(f":{arg.name}") + if isinstance(val, list): + live = [v for v in val if v is not NIL and v is not None] + items = [serialize(v) for v in live] + if not items: + parts.append("nil") + elif any(isinstance(v, SxExpr) for v in live): + parts.append("(<> " + " ".join(items) + ")") else: - parts.append(serialize(val)) + parts.append("(list " + " ".join(items) + ")") + else: + parts.append(serialize(val)) i += 2 else: result = await _aser(arg, env, ctx) @@ -996,7 +978,6 @@ _ASER_FORMS = { "fn": _assf_lambda, "define": _assf_define, "defstyle": _assf_define, - "defkeyframes": _assf_define, "defcomp": _assf_define, "defmacro": _assf_define, "defhandler": _assf_define, diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx index f3a9cd6..8f85e85 100644 --- a/shared/sx/ref/boot.sx +++ b/shared/sx/ref/boot.sx @@ -3,16 +3,14 @@ ;; ;; Handles the browser startup lifecycle: ;; 1. CSS tracking init -;; 2. Style dictionary loading (from