Remove CSSX style dictionary infrastructure — styling is just components

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 (<script type="text/sx-styles">), cookies, localStorage
- css/merge-styles primitives, inject-style-value, fnv1a-hash platform interface

Simplified:
- defstyle now binds any value (string, function) — no StyleValue type needed
- render-attrs no longer special-cases :style StyleValue → class conversion
- Boot sequence skips style dict init step

Preserved:
- tw.css parsing + CSS class delivery (SX-Css headers, <style id="sx-css">)
- All component infrastructure (defcomp, caching, bundling, deps)
- defstyle as a binding form for reusable class strings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 00:00:23 +00:00
parent 81d8e55fb0
commit a8bfff9e0b
30 changed files with 109 additions and 3164 deletions

View File

@@ -45,7 +45,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
# When True, _aser expands known components server-side instead of serializing
# them for client rendering. Set during page slot evaluation so Python-only
@@ -425,11 +425,6 @@ async def _asf_defstyle(expr, env, ctx):
return _sf_defstyle(expr, env)
async def _asf_defkeyframes(expr, env, ctx):
from .evaluator import _sf_defkeyframes
return _sf_defkeyframes(expr, env)
async def _asf_defmacro(expr, env, ctx):
from .evaluator import _sf_defmacro
return _sf_defmacro(expr, env)
@@ -568,7 +563,6 @@ _ASYNC_SPECIAL_FORMS: dict[str, Any] = {
"fn": _asf_lambda,
"define": _asf_define,
"defstyle": _asf_defstyle,
"defkeyframes": _asf_defkeyframes,
"defcomp": _asf_defcomp,
"defmacro": _asf_defmacro,
"defhandler": _asf_defhandler,
@@ -884,18 +878,6 @@ async def _arender_element(
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)
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"]
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)
@@ -1120,7 +1102,6 @@ _ASYNC_RENDER_FORMS: dict[str, Any] = {
"do": _arsf_begin,
"define": _arsf_define,
"defstyle": _arsf_define,
"defkeyframes": _arsf_define,
"defcomp": _arsf_define,
"defmacro": _arsf_define,
"defhandler": _arsf_define,
@@ -1441,41 +1422,35 @@ async def _aser_call(
try:
parts = [name]
extra_class: str | None = None # from :style StyleValue conversion
extra_class: str | None = None
i = 0
while i < len(args):
arg = args[i]
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:
# :style StyleValue → convert to :class and register CSS
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}")
# Plain list → serialize for the client.
# Rendered items (SxExpr) → wrap in (<> ...) fragment.
# Data items (dicts, strings, numbers) → (list ...)
# so the client gets an iterable array, not a
# DocumentFragment that breaks map/filter.
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}")
# Plain list → serialize for the client.
# Rendered items (SxExpr) → wrap in (<> ...) fragment.
# Data items (dicts, strings, numbers) → (list ...)
# so the client gets an iterable array, not a
# DocumentFragment that breaks map/filter.
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)
@@ -1725,7 +1700,6 @@ _ASER_FORMS: dict[str, Any] = {
"fn": _assf_lambda,
"define": _assf_define,
"defstyle": _assf_define,
"defkeyframes": _assf_define,
"defcomp": _assf_define,
"defmacro": _assf_define,
"defhandler": _assf_define,

View File

@@ -94,10 +94,10 @@ def validate_helper(service: str, name: str) -> None:
def validate_boundary_value(value: Any, context: str = "") -> None:
"""Validate that a value is an allowed SX boundary type.
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr, StyleValue.
Allowed: int, float, str, bool, None/NIL, list, dict, SxExpr.
NOT allowed: datetime, ORM models, Quart objects, raw callables.
"""
from .types import NIL, StyleValue
from .types import NIL
from .parser import SxExpr
if value is None or value is NIL:
@@ -106,8 +106,6 @@ def validate_boundary_value(value: Any, context: str = "") -> None:
return
if isinstance(value, SxExpr):
return
if isinstance(value, StyleValue):
return
if isinstance(value, list):
for item in value:
validate_boundary_value(item, context)

View File

@@ -147,48 +147,6 @@ def scan_classes_from_sx(source: str) -> set[str]:
return classes
def register_generated_rule(style_val: Any) -> None:
"""Register a generated StyleValue's CSS rules in the registry.
This allows generated class names (``sx-a3f2c1``) to flow through
the existing ``lookup_rules()`` → ``SX-Css`` delta pipeline.
"""
from .style_dict import CHILD_SELECTOR_ATOMS
cn = style_val.class_name
if cn in _REGISTRY:
return # already registered
parts: list[str] = []
# Base declarations
if style_val.declarations:
parts.append(f".{cn}{{{style_val.declarations}}}")
# Pseudo-class rules
for sel, decls in style_val.pseudo_rules:
if sel.startswith("::"):
parts.append(f".{cn}{sel}{{{decls}}}")
elif "&" in sel:
# group-hover pattern: ":is(.group:hover) &" → .group:hover .sx-abc
expanded = sel.replace("&", f".{cn}")
parts.append(f"{expanded}{{{decls}}}")
else:
parts.append(f".{cn}{sel}{{{decls}}}")
# Media-query rules
for query, decls in style_val.media_rules:
parts.append(f"@media {query}{{.{cn}{{{decls}}}}}")
# Keyframes
for _name, kf_rule in style_val.keyframes:
parts.append(kf_rule)
rule_text = "".join(parts)
order = len(_RULE_ORDER) + 10000 # after all tw.css rules
_REGISTRY[cn] = rule_text
_RULE_ORDER[cn] = order
def registry_loaded() -> bool:
"""True if the registry has been populated."""
return bool(_REGISTRY)

View File

@@ -493,9 +493,9 @@ def _sf_define(expr: list, env: dict) -> Any:
def _sf_defstyle(expr: list, env: dict) -> Any:
"""``(defstyle card-base (css :rounded-xl :bg-white :shadow))``
"""``(defstyle card-base ...)``
Evaluates body → StyleValue, binds to name in env.
Evaluates body and binds to name in env.
"""
if len(expr) < 3:
raise EvalError("defstyle requires name and body")
@@ -507,63 +507,6 @@ def _sf_defstyle(expr: list, env: dict) -> Any:
return value
def _sf_defkeyframes(expr: list, env: dict) -> Any:
"""``(defkeyframes fade-in (from (css :opacity-0)) (to (css :opacity-100)))``
Builds @keyframes rule from steps, registers it, and binds the animation.
"""
from .types import StyleValue
from .css_registry import register_generated_rule
from .style_dict import KEYFRAMES
if len(expr) < 3:
raise EvalError("defkeyframes requires name and at least one step")
name_sym = expr[1]
if not isinstance(name_sym, Symbol):
raise EvalError(f"defkeyframes name must be symbol, got {type(name_sym).__name__}")
kf_name = name_sym.name
# Build @keyframes rule from steps
steps: list[str] = []
for step_expr in expr[2:]:
if not isinstance(step_expr, list) or len(step_expr) < 2:
raise EvalError("defkeyframes step must be (selector (css ...))")
selector = step_expr[0]
if isinstance(selector, Symbol):
selector = selector.name
else:
selector = str(selector)
body = _trampoline(_eval(step_expr[1], env))
if isinstance(body, StyleValue):
decls = body.declarations
elif isinstance(body, str):
decls = body
else:
raise EvalError(f"defkeyframes step body must be css/string, got {type(body).__name__}")
steps.append(f"{selector}{{{decls}}}")
kf_rule = f"@keyframes {kf_name}{{{' '.join(steps)}}}"
# Register in KEYFRAMES so animate-{name} works
KEYFRAMES[kf_name] = kf_rule
# Clear resolver cache so new keyframes are picked up
from .style_resolver import _resolve_cached
_resolve_cached.cache_clear()
# Create a StyleValue for the animation property
import hashlib
h = hashlib.sha256(kf_rule.encode()).hexdigest()[:6]
sv = StyleValue(
class_name=f"sx-{h}",
declarations=f"animation-name:{kf_name}",
keyframes=((kf_name, kf_rule),),
)
register_generated_rule(sv)
env[kf_name] = sv
return sv
def _sf_defcomp(expr: list, env: dict) -> Component:
"""``(defcomp ~name (&key param1 param2 &rest children) body)``"""
if len(expr) < 4:
@@ -1060,7 +1003,6 @@ _SPECIAL_FORMS: dict[str, Any] = {
"fn": _sf_lambda,
"define": _sf_define,
"defstyle": _sf_defstyle,
"defkeyframes": _sf_defkeyframes,
"defcomp": _sf_defcomp,
"defrelation": _sf_defrelation,
"begin": _sf_begin,

View File

@@ -645,7 +645,6 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
</style>
</head>
<body class="bg-stone-50 text-stone-900">
<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>
<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>
<script type="text/sx-pages">{pages_sx}</script>
<script type="text/sx" data-mount="body">{page_sx}</script>
@@ -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, *,
'</style>\n'
'</head>\n'
'<body class="bg-stone-50 text-stone-900">\n'
f'<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>\n'
f'<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>\n'
f'<script type="text/sx-pages">{pages_sx}</script>\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:

View File

@@ -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:

View File

@@ -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):

View File

@@ -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
# ---------------------------------------------------------------------------

View File

@@ -38,7 +38,6 @@ Only these types may cross the host-SX boundary:
| list | `list` | `Array` | `Vec<SxValue>` |
| dict | `dict` | `Object` / `Map` | `HashMap<String, SxValue>` |
| 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -3,16 +3,14 @@
;;
;; Handles the browser startup lifecycle:
;; 1. CSS tracking init
;; 2. Style dictionary loading (from <script type="text/sx-styles">)
;; 3. Component script processing (from <script type="text/sx">)
;; 4. Hydration of [data-sx] elements
;; 5. Engine element processing
;; 2. Component script processing (from <script type="text/sx">)
;; 3. Hydration of [data-sx] elements
;; 4. Engine element processing
;;
;; Also provides the public mounting/hydration API:
;; mount, hydrate, update, render-component
;;
;; Depends on:
;; cssx.sx — load-style-dict
;; orchestration.sx — process-elements, engine-init
;; adapter-dom.sx — render-to-dom
;; render.sx — shared registries
@@ -275,58 +273,6 @@
(set-sx-comp-cookie hash))))))
;; --------------------------------------------------------------------------
;; Style dictionary initialization
;; --------------------------------------------------------------------------
(define init-style-dict
(fn ()
;; Process <script type="text/sx-styles"> tags with caching.
(let ((scripts (query-style-scripts)))
(for-each
(fn (s)
(when (not (is-processed? s "styles"))
(mark-processed! s "styles")
(let ((text (dom-text-content s))
(hash (dom-get-attr s "data-hash")))
(if (nil? hash)
;; No hash — just parse inline
(when (and text (not (empty? (trim text))))
(parse-and-load-style-dict text))
;; Hash-based caching
(let ((has-inline (and text (not (empty? (trim text))))))
(let ((cached-hash (local-storage-get "sx-styles-hash")))
(if (= cached-hash hash)
;; Cache hit
(if has-inline
(do
(local-storage-set "sx-styles-src" text)
(parse-and-load-style-dict text)
(log-info "styles: downloaded (cookie stale)"))
(let ((cached (local-storage-get "sx-styles-src")))
(if cached
(do
(parse-and-load-style-dict cached)
(log-info (str "styles: cached (" hash ")")))
(do
(clear-sx-styles-cookie)
(browser-reload)))))
;; Cache miss
(if has-inline
(do
(local-storage-set "sx-styles-hash" hash)
(local-storage-set "sx-styles-src" text)
(parse-and-load-style-dict text)
(log-info (str "styles: downloaded (" hash ")")))
(do
(local-storage-remove "sx-styles-hash")
(local-storage-remove "sx-styles-src")
(clear-sx-styles-cookie)
(browser-reload)))))
(set-sx-styles-cookie hash))))))
scripts))))
;; --------------------------------------------------------------------------
;; Page registry for client-side routing
;; --------------------------------------------------------------------------
@@ -375,7 +321,6 @@
(do
(log-info (str "sx-browser " SX_VERSION))
(init-css-tracking)
(init-style-dict)
(process-page-scripts)
(process-sx-scripts nil)
(sx-hydrate-elements nil)
@@ -389,9 +334,6 @@
;; From orchestration.sx:
;; process-elements, init-css-tracking
;;
;; From cssx.sx:
;; load-style-dict
;;
;; === DOM / Render ===
;; (resolve-mount-target target) → Element (string → querySelector, else identity)
;; (sx-render-with-env source extra-env) → DOM node (parse + render with componentEnv + extra)
@@ -420,7 +362,6 @@
;;
;; === Script queries ===
;; (query-sx-scripts root) → list of <script type="text/sx"> elements
;; (query-style-scripts) → list of <script type="text/sx-styles"> elements
;; (query-page-scripts) → list of <script type="text/sx-pages"> elements
;;
;; === localStorage ===
@@ -431,8 +372,6 @@
;; === Cookies ===
;; (set-sx-comp-cookie hash) → void
;; (clear-sx-comp-cookie) → void
;; (set-sx-styles-cookie hash) → void
;; (clear-sx-styles-cookie) → void
;;
;; === Env ===
;; (parse-env-attr el) → dict (parse data-sx-env JSON attr)
@@ -444,8 +383,6 @@
;; (log-parse-error label text err) → void (diagnostic parse error)
;;
;; === JSON parsing ===
;; (parse-and-load-style-dict text) → void (JSON.parse + load-style-dict)
;;
;; === Processing markers ===
;; (mark-processed! el key) → void
;; (is-processed? el key) → boolean

View File

@@ -191,10 +191,6 @@ class JSEmitter:
"ho-every": "hoEvery",
"ho-for-each": "hoForEach",
"sf-defstyle": "sfDefstyle",
"sf-defkeyframes": "sfDefkeyframes",
"build-keyframes": "buildKeyframes",
"style-value?": "isStyleValue",
"style-value-class": "styleValueClass",
"kf-name": "kfName",
"special-form?": "isSpecialForm",
"ho-form?": "isHoForm",
@@ -443,32 +439,6 @@ class JSEmitter:
"format-date": "formatDate",
"format-decimal": "formatDecimal",
"parse-int": "parseInt_",
# cssx.sx
"_style-atoms": "_styleAtoms",
"_pseudo-variants": "_pseudoVariants",
"_responsive-breakpoints": "_responsiveBreakpoints",
"_style-keyframes": "_styleKeyframes",
"_arbitrary-patterns": "_arbitraryPatterns",
"_child-selector-prefixes": "_childSelectorPrefixes",
"_style-cache": "_styleCache",
"_injected-styles": "_injectedStyles",
"load-style-dict": "loadStyleDict",
"split-variant": "splitVariant",
"resolve-atom": "resolveAtom",
"is-child-selector-atom?": "isChildSelectorAtom",
"hash-style": "hashStyle",
"resolve-style": "resolveStyle",
"merge-style-values": "mergeStyleValues",
"fnv1a-hash": "fnv1aHash",
"compile-regex": "compileRegex",
"regex-match": "regexMatch",
"regex-replace-groups": "regexReplaceGroups",
"make-style-value": "makeStyleValue_",
"style-value-declarations": "styleValueDeclarations",
"style-value-media-rules": "styleValueMediaRules",
"style-value-pseudo-rules": "styleValuePseudoRules",
"style-value-keyframes": "styleValueKeyframes_",
"inject-style-value": "injectStyleValue",
# boot.sx
"HEAD_HOIST_SELECTOR": "HEAD_HOIST_SELECTOR",
"hoist-head-elements-full": "hoistHeadElementsFull",
@@ -478,7 +448,6 @@ class JSEmitter:
"sx-render-component": "sxRenderComponent",
"process-sx-scripts": "processSxScripts",
"process-component-script": "processComponentScript",
"init-style-dict": "initStyleDict",
"SX_VERSION": "SX_VERSION",
"boot-init": "bootInit",
"resolve-suspense": "resolveSuspense",
@@ -490,21 +459,17 @@ class JSEmitter:
"set-document-title": "setDocumentTitle",
"remove-head-element": "removeHeadElement",
"query-sx-scripts": "querySxScripts",
"query-style-scripts": "queryStyleScripts",
"local-storage-get": "localStorageGet",
"local-storage-set": "localStorageSet",
"local-storage-remove": "localStorageRemove",
"set-sx-comp-cookie": "setSxCompCookie",
"clear-sx-comp-cookie": "clearSxCompCookie",
"set-sx-styles-cookie": "setSxStylesCookie",
"clear-sx-styles-cookie": "clearSxStylesCookie",
"parse-env-attr": "parseEnvAttr",
"store-env-attr": "storeEnvAttr",
"to-kebab": "toKebab",
"log-info": "logInfo",
"log-warn": "logWarn",
"log-parse-error": "logParseError",
"parse-and-load-style-dict": "parseAndLoadStyleDict",
"_page-routes": "_pageRoutes",
"process-page-scripts": "processPageScripts",
"query-page-scripts": "queryPageScripts",
@@ -1044,7 +1009,6 @@ ADAPTER_FILES = {
"dom": ("adapter-dom.sx", "adapter-dom"),
"engine": ("engine.sx", "engine"),
"orchestration": ("orchestration.sx","orchestration"),
"cssx": ("cssx.sx", "cssx"),
"boot": ("boot.sx", "boot"),
}
@@ -1052,8 +1016,7 @@ ADAPTER_FILES = {
ADAPTER_DEPS = {
"engine": ["dom"],
"orchestration": ["engine", "dom"],
"cssx": [],
"boot": ["dom", "engine", "orchestration", "cssx", "parser"],
"boot": ["dom", "engine", "orchestration", "parser"],
"parser": [],
}
@@ -1292,7 +1255,7 @@ ASYNC_IO_JS = '''
// define/defcomp/defmacro — eval for side effects
if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
hname === "defstyle" || hname === "defkeyframes" || hname === "defhandler") {
hname === "defstyle" || hname === "defhandler") {
trampoline(evalExpr(expr, env));
return null;
}
@@ -1414,11 +1377,7 @@ ASYNC_IO_JS = '''
})(attrName, attrVal);
} else {
if (!isNil(attrVal) && attrVal !== false) {
if (attrName === "class" && attrVal && attrVal._styleValue) {
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
el.setAttribute("class", (el.getAttribute("class") || "") + " " + attrVal.className);
} else if (contains(BOOLEAN_ATTRS, attrName)) {
if (contains(BOOLEAN_ATTRS, attrName)) {
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
} else if (attrVal === true) {
el.setAttribute(attrName, "");
@@ -1829,7 +1788,6 @@ def compile_ref_to_js(
"dom": PLATFORM_DOM_JS,
"engine": PLATFORM_ENGINE_PURE_JS,
"orchestration": PLATFORM_ORCHESTRATION_JS,
"cssx": PLATFORM_CSSX_JS,
"boot": PLATFORM_BOOT_JS,
}
@@ -1864,7 +1822,7 @@ def compile_ref_to_js(
("eval.sx", "eval"),
("render.sx", "render (core)"),
]
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
if name in adapter_set:
sx_files.append(ADAPTER_FILES[name])
for name in sorted(spec_mod_set):
@@ -1895,7 +1853,6 @@ def compile_ref_to_js(
has_dom = "dom" in adapter_set
has_engine = "engine" in adapter_set
has_orch = "orchestration" in adapter_set
has_cssx = "cssx" in adapter_set
has_boot = "boot" in adapter_set
has_parser = "parser" in adapter_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
@@ -1937,7 +1894,7 @@ def compile_ref_to_js(
# Platform JS for selected adapters
if not has_dom:
parts.append("\n var _hasDom = false;\n")
for name in ("dom", "engine", "orchestration", "cssx", "boot"):
for name in ("dom", "engine", "orchestration", "boot"):
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])
@@ -1946,7 +1903,7 @@ def compile_ref_to_js(
parts.append(CONTINUATIONS_JS)
if has_dom:
parts.append(ASYNC_IO_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps, has_router))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router))
parts.append(EPILOGUE)
from datetime import datetime, timezone
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -2019,15 +1976,6 @@ PREAMBLE = '''\
function RawHTML(html) { this.html = html; }
RawHTML.prototype._raw = true;
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
this.className = className;
this.declarations = declarations || "";
this.mediaRules = mediaRules || [];
this.pseudoRules = pseudoRules || [];
this.keyframes = keyframes || [];
}
StyleValue.prototype._styleValue = true;
function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; }
@@ -2224,30 +2172,6 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
''',
"stdlib.style": '''
// stdlib.style
PRIMITIVES["css"] = function() {
var atoms = [];
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
''',
"stdlib.debug": '''
// stdlib.debug
PRIMITIVES["assert"] = function(cond, msg) {
@@ -2295,7 +2219,6 @@ PLATFORM_JS_PRE = '''
if (x._component) return "component";
if (x._macro) return "macro";
if (x._raw) return "raw-html";
if (x._styleValue) return "style-value";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict";
@@ -2342,27 +2265,6 @@ PLATFORM_JS_PRE = '''
function isComponent(x) { return x != null && x._component === true; }
function isMacro(x) { return x != null && x._macro === true; }
function isStyleValue(x) { return x != null && x._styleValue === true; }
function styleValueClass(x) { return x.className; }
function styleValue_p(x) { return x != null && x._styleValue === true; }
function buildKeyframes(kfName, steps, env) {
// Platform implementation of defkeyframes
var parts = [];
for (var i = 0; i < steps.length; i++) {
var step = steps[i];
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
var body = trampoline(evalExpr(step[1], env));
var decls = isStyleValue(body) ? body.declarations : String(body);
parts.push(selector + "{" + decls + "}");
}
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
var cn = "sx-ref-kf-" + kfName;
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
env[kfName] = sv;
return sv;
}
function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; }
function envSet(env, name, val) { env[name] = val; }
@@ -2488,7 +2390,7 @@ PLATFORM_JS_POST = '''
function isSpecialForm(n) { return n in {
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
"defhandler":1,"begin":1,"do":1,
"quote":1,"quasiquote":1,"->":1,"set!":1
}; }
function isHoForm(n) { return n in {
@@ -2499,7 +2401,7 @@ PLATFORM_JS_POST = '''
function isDefinitionForm(name) {
return name === "define" || name === "defcomp" || name === "defmacro" ||
name === "defstyle" || name === "defkeyframes" || name === "defhandler";
name === "defstyle" || name === "defhandler";
}
function indexOf_(s, ch) {
@@ -2877,11 +2779,7 @@ PLATFORM_DOM_JS = """
var attrVal = trampoline(evalExpr(args[i + 1], env));
i++; // skip value
if (isNil(attrVal) || attrVal === false) continue;
if (attrName === "class" && attrVal && attrVal._styleValue) {
extraClasses.push(attrVal.className);
} else if (attrName === "style" && attrVal && attrVal._styleValue) {
extraClasses.push(attrVal.className);
} else if (contains(BOOLEAN_ATTRS, attrName)) {
if (contains(BOOLEAN_ATTRS, attrName)) {
if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
} else if (attrVal === true) {
el.setAttribute(attrName, "");
@@ -3763,102 +3661,6 @@ PLATFORM_ORCHESTRATION_JS = """
}
"""
PLATFORM_CSSX_JS = """
// =========================================================================
// Platform interface — CSSX (style dictionary)
// =========================================================================
function fnv1aHash(input) {
var h = 0x811c9dc5;
for (var i = 0; i < input.length; i++) {
h ^= input.charCodeAt(i);
h = (h * 0x01000193) >>> 0;
}
return h.toString(16).padStart(8, "0").substring(0, 6);
}
function compileRegex(pattern) {
try { return new RegExp(pattern); } catch (e) { return null; }
}
function regexMatch(re, s) {
if (!re) return NIL;
var m = s.match(re);
return m ? Array.prototype.slice.call(m) : NIL;
}
function regexReplaceGroups(tmpl, match) {
var result = tmpl;
for (var j = 1; j < match.length; j++) {
result = result.split("{" + (j - 1) + "}").join(match[j]);
}
return result;
}
function makeStyleValue_(cn, decls, media, pseudo, kf) {
return new StyleValue(cn, decls || "", media || [], pseudo || [], kf || []);
}
function styleValueDeclarations(sv) { return sv.declarations; }
function styleValueMediaRules(sv) { return sv.mediaRules; }
function styleValuePseudoRules(sv) { return sv.pseudoRules; }
function styleValueKeyframes_(sv) { return sv.keyframes; }
function injectStyleValue(sv, atoms) {
if (_injectedStyles[sv.className]) return;
_injectedStyles[sv.className] = true;
if (!_hasDom) return;
var cssTarget = document.getElementById("sx-css");
if (!cssTarget) return;
var rules = [];
// Child-selector atoms are now routed to pseudoRules by the resolver
// with selector ">:not(:first-child)", so base declarations are always
// applied directly to the class.
if (sv.declarations) {
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
if (sel.indexOf("&") >= 0) {
rules.push(sel.replace(/&/g, "." + sv.className) + "{" + decls + "}");
} else {
rules.push("." + sv.className + sel + "{" + decls + "}");
}
}
for (var mi = 0; mi < sv.mediaRules.length; mi++) {
rules.push("@media " + sv.mediaRules[mi][0] + "{." + sv.className + "{" + sv.mediaRules[mi][1] + "}}");
}
for (var ki = 0; ki < sv.keyframes.length; ki++) {
rules.push(sv.keyframes[ki][1]);
}
cssTarget.textContent += rules.join("");
}
// Replace stub css primitive with real CSSX implementation
PRIMITIVES["css"] = function() {
var atoms = [];
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return resolveStyle(atoms);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
return mergeStyleValues(valid);
};
"""
PLATFORM_BOOT_JS = """
// =========================================================================
// Platform interface — Boot (mount, hydrate, scripts, cookies)
@@ -3916,12 +3718,6 @@ PLATFORM_BOOT_JS = """
r.querySelectorAll('script[type="text/sx"]'));
}
function queryStyleScripts() {
if (!_hasDom) return [];
return Array.prototype.slice.call(
document.querySelectorAll('script[type="text/sx-styles"]'));
}
function queryPageScripts() {
if (!_hasDom) return [];
return Array.prototype.slice.call(
@@ -3953,14 +3749,6 @@ PLATFORM_BOOT_JS = """
if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax";
}
function setSxStylesCookie(hash) {
if (_hasDom) document.cookie = "sx-styles-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax";
}
function clearSxStylesCookie() {
if (_hasDom) document.cookie = "sx-styles-hash=;path=/;max-age=0;SameSite=Lax";
}
// --- Env helpers ---
function parseEnvAttr(el) {
@@ -4009,10 +3797,6 @@ PLATFORM_BOOT_JS = """
}
}
function parseAndLoadStyleDict(text) {
try { loadStyleDict(JSON.parse(text)); }
catch (e) { if (typeof console !== "undefined") console.warn("[sx-ref] style dict parse error", e); }
}
"""
def fixups_js(has_html, has_sx, has_dom):
@@ -4039,7 +3823,7 @@ def fixups_js(has_html, has_sx, has_dom):
return "\n".join(lines)
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
if has_parser:
parser = '''

View File

@@ -199,11 +199,6 @@ class PyEmitter:
"ho-every": "ho_every",
"ho-for-each": "ho_for_each",
"sf-defstyle": "sf_defstyle",
"sf-defkeyframes": "sf_defkeyframes",
"build-keyframes": "build_keyframes",
"style-value?": "is_style_value",
"style-value-class": "style_value_class",
"kf-name": "kf_name",
"special-form?": "is_special_form",
"ho-form?": "is_ho_form",
"strip-prefix": "strip_prefix",
@@ -1080,7 +1075,7 @@ from typing import Any
# =========================================================================
from shared.sx.types import (
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
)
from shared.sx.parser import SxExpr
@@ -1189,8 +1184,6 @@ def type_of(x):
return "macro"
if isinstance(x, _RawHTML):
return "raw-html"
if isinstance(x, StyleValue):
return "style-value"
if isinstance(x, Continuation):
return "continuation"
if isinstance(x, list):
@@ -1355,14 +1348,6 @@ def is_macro(x):
return isinstance(x, Macro)
def is_style_value(x):
return isinstance(x, StyleValue)
def style_value_class(x):
return x.class_name
def env_has(env, name):
return name in env
@@ -1513,8 +1498,6 @@ def serialize(val):
if t == "raw-html":
escaped = escape_string(raw_html_content(val))
return '(raw! "' + escaped + '")'
if t == "style-value":
return '"' + style_value_class(val) + '"'
if t == "list":
if not val:
return "()"
@@ -1534,7 +1517,7 @@ def serialize(val):
_SPECIAL_FORM_NAMES = frozenset([
"if", "when", "cond", "case", "and", "or",
"let", "let*", "lambda", "fn",
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
"define", "defcomp", "defmacro", "defstyle",
"defhandler", "defpage", "defquery", "defaction", "defrelation",
"begin", "do", "quote", "quasiquote",
"->", "set!",
@@ -1696,7 +1679,7 @@ def aser_special(name, expr, env):
fn(item)
return results if results else NIL
# Definition forms — evaluate for side effects
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
if name in ("define", "defcomp", "defmacro", "defstyle",
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
trampoline(eval_expr(expr, env))
return NIL

View File

@@ -123,4 +123,4 @@
(define-boundary-types
(list "number" "string" "boolean" "nil" "keyword"
"list" "dict" "sx-source" "style-value"))
"list" "dict" "sx-source"))

View File

@@ -1,317 +0,0 @@
;; ==========================================================================
;; cssx.sx — On-demand CSS style dictionary
;;
;; Resolves keyword atoms (e.g. :flex, :gap-4, :hover:bg-sky-200) into
;; StyleValue objects with content-addressed class names. CSS rules are
;; injected into the document on first use.
;;
;; The style dictionary is loaded from a JSON blob (typically served
;; inline in a <script type="text/sx-styles"> tag) containing:
;; a — atom → CSS declarations map
;; v — pseudo-variant → CSS pseudo-selector map
;; b — responsive breakpoint → media query map
;; k — keyframe name → @keyframes rule map
;; p — arbitrary patterns: [[regex, template], ...]
;; c — child selector prefixes: ["space-x-", "space-y-", ...]
;;
;; Depends on:
;; render.sx — StyleValue type
;; ==========================================================================
;; --------------------------------------------------------------------------
;; State — populated by load-style-dict
;; --------------------------------------------------------------------------
(define _style-atoms (dict))
(define _pseudo-variants (dict))
(define _responsive-breakpoints (dict))
(define _style-keyframes (dict))
(define _arbitrary-patterns (list))
(define _child-selector-prefixes (list))
(define _style-cache (dict))
(define _injected-styles (dict))
;; --------------------------------------------------------------------------
;; Load style dictionary from parsed JSON data
;; --------------------------------------------------------------------------
(define load-style-dict
(fn (data)
(set! _style-atoms (or (get data "a") (dict)))
(set! _pseudo-variants (or (get data "v") (dict)))
(set! _responsive-breakpoints (or (get data "b") (dict)))
(set! _style-keyframes (or (get data "k") (dict)))
(set! _child-selector-prefixes (or (get data "c") (list)))
;; Compile arbitrary patterns from [regex, template] pairs
(set! _arbitrary-patterns
(map
(fn (pair)
(dict "re" (compile-regex (str "^" (first pair) "$"))
"tmpl" (nth pair 1)))
(or (get data "p") (list))))
;; Clear cache on reload
(set! _style-cache (dict))))
;; --------------------------------------------------------------------------
;; Variant splitting
;; --------------------------------------------------------------------------
(define split-variant
(fn (atom)
;; Parse variant prefixes: "sm:hover:bg-sky-200" → ["sm:hover", "bg-sky-200"]
;; Returns [variant, base] where variant is nil for no variant.
;; Check responsive prefix first
(let ((result nil))
(for-each
(fn (bp)
(when (nil? result)
(let ((prefix (str bp ":")))
(when (starts-with? atom prefix)
(let ((rest-atom (slice atom (len prefix))))
;; Check for compound variant (sm:hover:...)
(let ((inner-match nil))
(for-each
(fn (pv)
(when (nil? inner-match)
(let ((inner-prefix (str pv ":")))
(when (starts-with? rest-atom inner-prefix)
(set! inner-match
(list (str bp ":" pv)
(slice rest-atom (len inner-prefix))))))))
(keys _pseudo-variants))
(set! result
(or inner-match (list bp rest-atom)))))))))
(keys _responsive-breakpoints))
(when (nil? result)
;; Check pseudo variants
(for-each
(fn (pv)
(when (nil? result)
(let ((prefix (str pv ":")))
(when (starts-with? atom prefix)
(set! result (list pv (slice atom (len prefix))))))))
(keys _pseudo-variants)))
(or result (list nil atom)))))
;; --------------------------------------------------------------------------
;; Atom resolution
;; --------------------------------------------------------------------------
(define resolve-atom
(fn (atom)
;; Look up atom → CSS declarations string, or nil
(let ((decls (dict-get _style-atoms atom)))
(if (not (nil? decls))
decls
;; Dynamic keyframes: animate-{name}
(if (starts-with? atom "animate-")
(let ((kf-name (slice atom 8)))
(if (dict-has? _style-keyframes kf-name)
(str "animation-name:" kf-name)
nil))
;; Try arbitrary patterns
(let ((match-result nil))
(for-each
(fn (pat)
(when (nil? match-result)
(let ((m (regex-match (get pat "re") atom)))
(when m
(set! match-result
(regex-replace-groups (get pat "tmpl") m))))))
_arbitrary-patterns)
match-result))))))
;; --------------------------------------------------------------------------
;; Child selector detection
;; --------------------------------------------------------------------------
(define is-child-selector-atom?
(fn (atom)
(some
(fn (prefix) (starts-with? atom prefix))
_child-selector-prefixes)))
;; --------------------------------------------------------------------------
;; FNV-1a 32-bit hash → 6 hex chars
;; --------------------------------------------------------------------------
(define hash-style
(fn (input)
;; FNV-1a 32-bit hash for content-addressed class names
(fnv1a-hash input)))
;; --------------------------------------------------------------------------
;; Full style resolution pipeline
;; --------------------------------------------------------------------------
(define resolve-style
(fn (atoms)
;; Resolve a list of atom strings into a StyleValue.
;; Uses content-addressed caching.
(let ((key (join "\0" atoms)))
(let ((cached (dict-get _style-cache key)))
(if (not (nil? cached))
cached
;; Resolve each atom
(let ((base-decls (list))
(media-rules (list))
(pseudo-rules (list))
(kf-needed (list)))
(for-each
(fn (a)
(when a
(let ((clean (if (starts-with? a ":") (slice a 1) a)))
(let ((parts (split-variant clean)))
(let ((variant (first parts))
(base (nth parts 1))
(decls (resolve-atom base)))
(when decls
;; Check keyframes
(when (starts-with? base "animate-")
(let ((kf-name (slice base 8)))
(when (dict-has? _style-keyframes kf-name)
(append! kf-needed
(list kf-name (dict-get _style-keyframes kf-name))))))
(cond
(nil? variant)
(if (is-child-selector-atom? base)
(append! pseudo-rules
(list ">:not(:first-child)" decls))
(append! base-decls decls))
(dict-has? _responsive-breakpoints variant)
(append! media-rules
(list (dict-get _responsive-breakpoints variant) decls))
(dict-has? _pseudo-variants variant)
(append! pseudo-rules
(list (dict-get _pseudo-variants variant) decls))
;; Compound variant: "sm:hover"
:else
(let ((vparts (split variant ":"))
(media-part nil)
(pseudo-part nil))
(for-each
(fn (vp)
(cond
(dict-has? _responsive-breakpoints vp)
(set! media-part (dict-get _responsive-breakpoints vp))
(dict-has? _pseudo-variants vp)
(set! pseudo-part (dict-get _pseudo-variants vp))))
vparts)
(when media-part
(append! media-rules (list media-part decls)))
(when pseudo-part
(append! pseudo-rules (list pseudo-part decls)))
(when (and (nil? media-part) (nil? pseudo-part))
(append! base-decls decls))))))))))
atoms)
;; Build hash input
(let ((hash-input (join ";" base-decls)))
(for-each
(fn (mr)
(set! hash-input
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
media-rules)
(for-each
(fn (pr)
(set! hash-input
(str hash-input (first pr) "{" (nth pr 1) "}")))
pseudo-rules)
(for-each
(fn (kf)
(set! hash-input (str hash-input (nth kf 1))))
kf-needed)
(let ((cn (str "sx-" (hash-style hash-input)))
(sv (make-style-value cn
(join ";" base-decls)
media-rules
pseudo-rules
kf-needed)))
(dict-set! _style-cache key sv)
;; Inject CSS rules
(inject-style-value sv atoms)
sv))))))))
;; --------------------------------------------------------------------------
;; Merge multiple StyleValues
;; --------------------------------------------------------------------------
(define merge-style-values
(fn (styles)
(if (= (len styles) 1)
(first styles)
(let ((all-decls (list))
(all-media (list))
(all-pseudo (list))
(all-kf (list)))
(for-each
(fn (sv)
(when (style-value-declarations sv)
(append! all-decls (style-value-declarations sv)))
(set! all-media (concat all-media (style-value-media-rules sv)))
(set! all-pseudo (concat all-pseudo (style-value-pseudo-rules sv)))
(set! all-kf (concat all-kf (style-value-keyframes sv))))
styles)
(let ((hash-input (join ";" all-decls)))
(for-each
(fn (mr)
(set! hash-input
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
all-media)
(for-each
(fn (pr)
(set! hash-input
(str hash-input (first pr) "{" (nth pr 1) "}")))
all-pseudo)
(for-each
(fn (kf)
(set! hash-input (str hash-input (nth kf 1))))
all-kf)
(let ((cn (str "sx-" (hash-style hash-input)))
(merged (make-style-value cn
(join ";" all-decls)
all-media all-pseudo all-kf)))
(inject-style-value merged (list))
merged))))))
;; --------------------------------------------------------------------------
;; Platform interface — CSSX
;; --------------------------------------------------------------------------
;;
;; Hash:
;; (fnv1a-hash input) → 6-char hex string (FNV-1a 32-bit)
;;
;; Regex:
;; (compile-regex pattern) → compiled regex object
;; (regex-match re str) → match array or nil
;; (regex-replace-groups tmpl match) → string with {0},{1},... replaced
;;
;; StyleValue construction:
;; (make-style-value cn decls media pseudo kf) → StyleValue object
;; (style-value-declarations sv) → declarations string
;; (style-value-media-rules sv) → list of [query, decls] pairs
;; (style-value-pseudo-rules sv) → list of [selector, decls] pairs
;; (style-value-keyframes sv) → list of [name, rule] pairs
;;
;; CSS injection:
;; (inject-style-value sv atoms) → void (append CSS rules to <style id="sx-css">)
;; --------------------------------------------------------------------------

View File

@@ -143,7 +143,6 @@
(= name "defcomp") (sf-defcomp args env)
(= name "defmacro") (sf-defmacro args env)
(= name "defstyle") (sf-defstyle args env)
(= name "defkeyframes") (sf-defkeyframes args env)
(= name "defhandler") (sf-defhandler args env)
(= name "defpage") (sf-defpage args env)
(= name "defquery") (sf-defquery args env)
@@ -559,23 +558,13 @@
(define sf-defstyle
(fn (args env)
;; (defstyle name expr) — bind name to evaluated expr (typically a StyleValue)
;; (defstyle name expr) — bind name to evaluated expr (string, function, etc.)
(let ((name-sym (first args))
(value (trampoline (eval-expr (nth args 1) env))))
(env-set! env (symbol-name name-sym) value)
value)))
(define sf-defkeyframes
(fn (args env)
;; (defkeyframes name (selector body) ...) — build @keyframes rule,
;; register in keyframes dict, return StyleValue.
;; Delegates to platform: build-keyframes returns a StyleValue.
(let ((kf-name (symbol-name (first args)))
(steps (rest args)))
(build-keyframes kf-name steps env))))
(define sf-begin
(fn (args env)
(if (empty? args)
@@ -931,9 +920,6 @@
;; (zip lists...) → list of tuples
;;
;;
;; CSSX (style system):
;; (build-keyframes name steps env) → StyleValue (platform builds @keyframes)
;;
;; Dynamic wind (for dynamic-wind):
;; (push-wind! before after) → void (push wind record onto stack)
;; (pop-wind!) → void (pop wind record from stack)

View File

@@ -541,18 +541,6 @@
;; Stdlib — Style
;; --------------------------------------------------------------------------
(define-module :stdlib.style)
(define-primitive "css"
:params (&rest atoms)
:returns "style-value"
:doc "Resolve style atoms to a StyleValue with className and CSS declarations.
Atoms are keywords or strings: (css :flex :gap-4 :hover:bg-sky-200).")
(define-primitive "merge-styles"
:params (&rest styles)
:returns "style-value"
:doc "Merge multiple StyleValues into one combined StyleValue.")
;; --------------------------------------------------------------------------

View File

@@ -74,7 +74,7 @@
(define definition-form?
(fn (name)
(or (= name "define") (= name "defcomp") (= name "defmacro")
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))))
(= name "defstyle") (= name "defhandler"))))
(define parse-element-args
@@ -116,9 +116,6 @@
""
;; Nil values — skip
(nil? val) ""
;; StyleValue on :style → emit as class
(and (= key "style") (style-value? val))
(str " class=\"" (style-value-class val) "\"")
;; Normal attr
:else (str " " key "=\"" (escape-attr (str val)) "\""))))
(keys attrs)))))
@@ -202,10 +199,6 @@
;; (escape-attr s) → attribute-value-escaped string
;; (raw-html-content r) → unwrap RawHTML marker to string
;;
;; StyleValue:
;; (style-value? x) → boolean (is x a StyleValue?)
;; (style-value-class sv) → string (CSS class name)
;;
;; Serialization:
;; (serialize val) → SX source string representation of val
;;

View File

@@ -363,20 +363,12 @@
;; --------------------------------------------------------------------------
(define-special-form "defstyle"
:syntax (defstyle name atoms ...)
:doc "Define a named style. Evaluates atoms to a StyleValue and binds
it to name in the environment."
:syntax (defstyle name expr)
:doc "Define a named style value. Evaluates expr and binds the result
to name in the environment. The value is typically a class string
or a function that returns class strings."
:tail-position "none"
:example "(defstyle card-style :rounded-lg :shadow-md :p-4 :bg-white)")
(define-special-form "defkeyframes"
:syntax (defkeyframes name steps ...)
:doc "Define a CSS @keyframes animation. Steps are (percentage properties ...)
pairs. Produces a StyleValue with the animation name and keyframe rules."
:tail-position "none"
:example "(defkeyframes fade-in
(0 :opacity-0)
(100 :opacity-100))")
:example "(defstyle card-style \"rounded-lg shadow-md p-4 bg-white\")")
(define-special-form "defhandler"
:syntax (defhandler name (&key params ...) body)

View File

@@ -18,7 +18,7 @@ from typing import Any
# =========================================================================
from shared.sx.types import (
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro, StyleValue,
NIL, Symbol, Keyword, Lambda, Component, Continuation, Macro,
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
)
from shared.sx.parser import SxExpr
@@ -126,8 +126,6 @@ def type_of(x):
return "macro"
if isinstance(x, _RawHTML):
return "raw-html"
if isinstance(x, StyleValue):
return "style-value"
if isinstance(x, Continuation):
return "continuation"
if isinstance(x, list):
@@ -292,14 +290,6 @@ def is_macro(x):
return isinstance(x, Macro)
def is_style_value(x):
return isinstance(x, StyleValue)
def style_value_class(x):
return x.class_name
def env_has(env, name):
return name in env
@@ -450,8 +440,6 @@ def serialize(val):
if t == "raw-html":
escaped = escape_string(raw_html_content(val))
return '(raw! "' + escaped + '")'
if t == "style-value":
return '"' + style_value_class(val) + '"'
if t == "list":
if not val:
return "()"
@@ -471,7 +459,7 @@ def serialize(val):
_SPECIAL_FORM_NAMES = frozenset([
"if", "when", "cond", "case", "and", "or",
"let", "let*", "lambda", "fn",
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
"define", "defcomp", "defmacro", "defstyle",
"defhandler", "defpage", "defquery", "defaction", "defrelation",
"begin", "do", "quote", "quasiquote",
"->", "set!",
@@ -633,7 +621,7 @@ def aser_special(name, expr, env):
fn(item)
return results if results else NIL
# Definition forms — evaluate for side effects
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
if name in ("define", "defcomp", "defmacro", "defstyle",
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
trampoline(eval_expr(expr, env))
return NIL
@@ -955,7 +943,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
# eval-list
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env)))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
# eval-call
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
@@ -1051,9 +1039,6 @@ def parse_macro_params(params_expr):
# sf-defstyle
sf_defstyle = lambda args, env: (lambda name_sym: (lambda value: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args))
# sf-defkeyframes
sf_defkeyframes = lambda args, env: (lambda kf_name: (lambda steps: build_keyframes(kf_name, steps, env))(rest(args)))(symbol_name(first(args)))
# sf-begin
sf_begin = lambda args, env: (NIL if sx_truthy(empty_p(args)) else _sx_begin(for_each(lambda e: trampoline(eval_expr(e, env)), slice(args, 0, (len(args) - 1))), make_thunk(last(args), env)))
@@ -1164,13 +1149,13 @@ VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'li
BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert', 'ismap', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected']
# definition-form?
is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defkeyframes') if sx_truthy((name == 'defkeyframes')) else (name == 'defhandler'))))))
is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else (name == 'defhandler')))))
# parse-element-args
parse_element_args = lambda args, env: (lambda attrs: (lambda children: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin(_sx_dict_set(attrs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), [attrs, children]))([]))({})
# render-attrs
render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else (sx_str(' class="', style_value_class(val), '"') if sx_truthy(((key == 'style') if not sx_truthy((key == 'style')) else is_style_value(val))) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"'))))))(dict_get(attrs, key)), keys(attrs)))
render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"')))))(dict_get(attrs, key)), keys(attrs)))
# eval-cond
eval_cond = lambda clauses, env: (eval_cond_scheme(clauses, env) if sx_truthy(((not sx_truthy(empty_p(clauses))) if not sx_truthy((not sx_truthy(empty_p(clauses)))) else ((type_of(first(clauses)) == 'list') if not sx_truthy((type_of(first(clauses)) == 'list')) else (len(first(clauses)) == 2)))) else eval_cond_clojure(clauses, env))
@@ -1191,10 +1176,10 @@ process_bindings = lambda bindings, env: (lambda local: _sx_begin(for_each(lambd
render_to_html = lambda expr, env: _sx_case(type_of(expr), [('nil', lambda: ''), ('string', lambda: escape_html(expr)), ('number', lambda: sx_str(expr)), ('boolean', lambda: ('true' if sx_truthy(expr) else 'false')), ('list', lambda: ('' if sx_truthy(empty_p(expr)) else render_list_to_html(expr, env))), ('symbol', lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env)), ('keyword', lambda: escape_html(keyword_name(expr))), ('raw-html', lambda: raw_html_content(expr)), (None, lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env))])
# render-value-to-html
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), ('style-value', lambda: style_value_class(val)), (None, lambda: escape_html(sx_str(val)))])
render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), (None, lambda: escape_html(sx_str(val)))])
# RENDER_HTML_FORMS
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defkeyframes', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
# render-html-form?
is_render_html_form = lambda name: contains_p(RENDER_HTML_FORMS, name)

View File

@@ -1,782 +0,0 @@
"""
Style dictionary — maps keyword atoms to CSS declarations.
Pure data. Each key is a Tailwind-compatible class name (used as an sx keyword
atom in ``(css :flex :gap-4 :p-2)``), and each value is the CSS declaration(s)
that class produces. Declarations are self-contained — no ``--tw-*`` custom
properties needed.
Generated from the codebase's tw.css via ``css_registry.py`` then simplified
to remove Tailwind v3 variable indirection.
Used by:
- ``style_resolver.py`` (server) — resolves ``(css ...)`` to StyleValue
- ``sx.js`` (client) — same resolution, cached in localStorage
"""
from __future__ import annotations
# ═══════════════════════════════════════════════════════════════════════════
# Base atoms — keyword → CSS declarations
# ═══════════════════════════════════════════════════════════════════════════
#
# ~466 atoms covering all utilities used across the codebase.
# Variants (hover:*, sm:*, focus:*, etc.) are NOT stored here — the
# resolver splits "hover:bg-sky-200" into variant="hover" + atom="bg-sky-200"
# and wraps the declaration in the appropriate pseudo/media rule.
STYLE_ATOMS: dict[str, str] = {
# ── Display ──────────────────────────────────────────────────────────
"block": "display:block",
"inline-block": "display:inline-block",
"inline": "display:inline",
"flex": "display:flex",
"inline-flex": "display:inline-flex",
"table": "display:table",
"table-row": "display:table-row",
"grid": "display:grid",
"contents": "display:contents",
"hidden": "display:none",
# ── Position ─────────────────────────────────────────────────────────
"static": "position:static",
"fixed": "position:fixed",
"absolute": "position:absolute",
"relative": "position:relative",
"inset-0": "inset:0",
"top-0": "top:0",
"top-1/2": "top:50%",
"top-2": "top:.5rem",
"top-20": "top:5rem",
"top-[8px]": "top:8px",
"top-full": "top:100%",
"right-2": "right:.5rem",
"right-[8px]": "right:8px",
"bottom-full": "bottom:100%",
"left-1/2": "left:50%",
"left-2": "left:.5rem",
"-right-2": "right:-.5rem",
"-right-3": "right:-.75rem",
"-top-1.5": "top:-.375rem",
"-top-2": "top:-.5rem",
# ── Z-Index ──────────────────────────────────────────────────────────
"z-10": "z-index:10",
"z-40": "z-index:40",
"z-50": "z-index:50",
# ── Grid ─────────────────────────────────────────────────────────────
"grid-cols-1": "grid-template-columns:repeat(1,minmax(0,1fr))",
"grid-cols-2": "grid-template-columns:repeat(2,minmax(0,1fr))",
"grid-cols-3": "grid-template-columns:repeat(3,minmax(0,1fr))",
"grid-cols-4": "grid-template-columns:repeat(4,minmax(0,1fr))",
"grid-cols-5": "grid-template-columns:repeat(5,minmax(0,1fr))",
"grid-cols-6": "grid-template-columns:repeat(6,minmax(0,1fr))",
"grid-cols-7": "grid-template-columns:repeat(7,minmax(0,1fr))",
"grid-cols-12": "grid-template-columns:repeat(12,minmax(0,1fr))",
"col-span-2": "grid-column:span 2/span 2",
"col-span-3": "grid-column:span 3/span 3",
"col-span-4": "grid-column:span 4/span 4",
"col-span-5": "grid-column:span 5/span 5",
"col-span-12": "grid-column:span 12/span 12",
"col-span-full": "grid-column:1/-1",
# ── Flexbox ──────────────────────────────────────────────────────────
"flex-row": "flex-direction:row",
"flex-col": "flex-direction:column",
"flex-wrap": "flex-wrap:wrap",
"flex-0": "flex:0",
"flex-1": "flex:1 1 0%",
"flex-shrink-0": "flex-shrink:0",
"shrink-0": "flex-shrink:0",
"flex-shrink": "flex-shrink:1",
# ── Alignment ────────────────────────────────────────────────────────
"items-start": "align-items:flex-start",
"items-end": "align-items:flex-end",
"items-center": "align-items:center",
"items-baseline": "align-items:baseline",
"justify-start": "justify-content:flex-start",
"justify-end": "justify-content:flex-end",
"justify-center": "justify-content:center",
"justify-between": "justify-content:space-between",
"self-start": "align-self:flex-start",
"self-center": "align-self:center",
"place-items-center": "place-items:center",
# ── Gap ───────────────────────────────────────────────────────────────
"gap-px": "gap:1px",
"gap-0.5": "gap:.125rem",
"gap-1": "gap:.25rem",
"gap-1.5": "gap:.375rem",
"gap-2": "gap:.5rem",
"gap-3": "gap:.75rem",
"gap-4": "gap:1rem",
"gap-5": "gap:1.25rem",
"gap-6": "gap:1.5rem",
"gap-8": "gap:2rem",
"gap-[4px]": "gap:4px",
"gap-[8px]": "gap:8px",
"gap-[16px]": "gap:16px",
"gap-x-3": "column-gap:.75rem",
"gap-y-1": "row-gap:.25rem",
# ── Margin ───────────────────────────────────────────────────────────
"m-0": "margin:0",
"m-2": "margin:.5rem",
"mx-1": "margin-left:.25rem;margin-right:.25rem",
"mx-2": "margin-left:.5rem;margin-right:.5rem",
"mx-4": "margin-left:1rem;margin-right:1rem",
"mx-auto": "margin-left:auto;margin-right:auto",
"my-3": "margin-top:.75rem;margin-bottom:.75rem",
"-mb-px": "margin-bottom:-1px",
"mb-1": "margin-bottom:.25rem",
"mb-2": "margin-bottom:.5rem",
"mb-3": "margin-bottom:.75rem",
"mb-4": "margin-bottom:1rem",
"mb-6": "margin-bottom:1.5rem",
"mb-8": "margin-bottom:2rem",
"mb-12": "margin-bottom:3rem",
"mb-[8px]": "margin-bottom:8px",
"mb-[24px]": "margin-bottom:24px",
"ml-1": "margin-left:.25rem",
"ml-2": "margin-left:.5rem",
"ml-4": "margin-left:1rem",
"ml-auto": "margin-left:auto",
"mr-1": "margin-right:.25rem",
"mr-2": "margin-right:.5rem",
"mr-3": "margin-right:.75rem",
"mt-0.5": "margin-top:.125rem",
"mt-1": "margin-top:.25rem",
"mt-2": "margin-top:.5rem",
"mt-3": "margin-top:.75rem",
"mt-4": "margin-top:1rem",
"mt-5": "margin-top:1.25rem",
"mt-6": "margin-top:1.5rem",
"mt-8": "margin-top:2rem",
"mt-[8px]": "margin-top:8px",
"mt-[16px]": "margin-top:16px",
"mt-[32px]": "margin-top:32px",
# ── Padding ──────────────────────────────────────────────────────────
"p-0": "padding:0",
"p-1": "padding:.25rem",
"p-1.5": "padding:.375rem",
"p-2": "padding:.5rem",
"p-3": "padding:.75rem",
"p-4": "padding:1rem",
"p-5": "padding:1.25rem",
"p-6": "padding:1.5rem",
"p-8": "padding:2rem",
"px-1": "padding-left:.25rem;padding-right:.25rem",
"px-1.5": "padding-left:.375rem;padding-right:.375rem",
"px-2": "padding-left:.5rem;padding-right:.5rem",
"px-2.5": "padding-left:.625rem;padding-right:.625rem",
"px-3": "padding-left:.75rem;padding-right:.75rem",
"px-4": "padding-left:1rem;padding-right:1rem",
"px-6": "padding-left:1.5rem;padding-right:1.5rem",
"px-[8px]": "padding-left:8px;padding-right:8px",
"px-[12px]": "padding-left:12px;padding-right:12px",
"px-[16px]": "padding-left:16px;padding-right:16px",
"px-[20px]": "padding-left:20px;padding-right:20px",
"py-0.5": "padding-top:.125rem;padding-bottom:.125rem",
"py-1": "padding-top:.25rem;padding-bottom:.25rem",
"py-1.5": "padding-top:.375rem;padding-bottom:.375rem",
"py-2": "padding-top:.5rem;padding-bottom:.5rem",
"py-3": "padding-top:.75rem;padding-bottom:.75rem",
"py-4": "padding-top:1rem;padding-bottom:1rem",
"py-6": "padding-top:1.5rem;padding-bottom:1.5rem",
"py-8": "padding-top:2rem;padding-bottom:2rem",
"py-12": "padding-top:3rem;padding-bottom:3rem",
"py-16": "padding-top:4rem;padding-bottom:4rem",
"py-[6px]": "padding-top:6px;padding-bottom:6px",
"py-[12px]": "padding-top:12px;padding-bottom:12px",
"pb-1": "padding-bottom:.25rem",
"pb-2": "padding-bottom:.5rem",
"pb-3": "padding-bottom:.75rem",
"pb-4": "padding-bottom:1rem",
"pb-6": "padding-bottom:1.5rem",
"pb-8": "padding-bottom:2rem",
"pb-[48px]": "padding-bottom:48px",
"pl-2": "padding-left:.5rem",
"pl-3": "padding-left:.75rem",
"pl-5": "padding-left:1.25rem",
"pl-6": "padding-left:1.5rem",
"pr-1": "padding-right:.25rem",
"pr-2": "padding-right:.5rem",
"pr-4": "padding-right:1rem",
"pt-2": "padding-top:.5rem",
"pt-3": "padding-top:.75rem",
"pt-4": "padding-top:1rem",
"pt-[16px]": "padding-top:16px",
# ── Width ────────────────────────────────────────────────────────────
"w-1": "width:.25rem",
"w-2": "width:.5rem",
"w-4": "width:1rem",
"w-5": "width:1.25rem",
"w-6": "width:1.5rem",
"w-8": "width:2rem",
"w-10": "width:2.5rem",
"w-11": "width:2.75rem",
"w-12": "width:3rem",
"w-14": "width:3.5rem",
"w-16": "width:4rem",
"w-20": "width:5rem",
"w-24": "width:6rem",
"w-28": "width:7rem",
"w-32": "width:8rem",
"w-40": "width:10rem",
"w-48": "width:12rem",
"w-56": "width:14rem",
"w-1/2": "width:50%",
"w-1/3": "width:33.333333%",
"w-1/4": "width:25%",
"w-1/6": "width:16.666667%",
"w-2/6": "width:33.333333%",
"w-3/4": "width:75%",
"w-full": "width:100%",
"w-auto": "width:auto",
"w-[1em]": "width:1em",
"w-[32px]": "width:32px",
# ── Height ───────────────────────────────────────────────────────────
"h-2": "height:.5rem",
"h-4": "height:1rem",
"h-5": "height:1.25rem",
"h-6": "height:1.5rem",
"h-8": "height:2rem",
"h-10": "height:2.5rem",
"h-12": "height:3rem",
"h-14": "height:3.5rem",
"h-14": "height:3.5rem",
"h-16": "height:4rem",
"h-24": "height:6rem",
"h-28": "height:7rem",
"h-48": "height:12rem",
"h-64": "height:16rem",
"h-full": "height:100%",
"h-[1em]": "height:1em",
"h-[30vh]": "height:30vh",
"h-[32px]": "height:32px",
"h-[60vh]": "height:60vh",
# ── Min/Max Dimensions ───────────────────────────────────────────────
"min-w-0": "min-width:0",
"min-w-full": "min-width:100%",
"min-w-[1.25rem]": "min-width:1.25rem",
"min-w-[180px]": "min-width:180px",
"min-h-0": "min-height:0",
"min-h-20": "min-height:5rem",
"min-h-[3rem]": "min-height:3rem",
"min-h-[50vh]": "min-height:50vh",
"max-w-xs": "max-width:20rem",
"max-w-md": "max-width:28rem",
"max-w-lg": "max-width:32rem",
"max-w-2xl": "max-width:42rem",
"max-w-3xl": "max-width:48rem",
"max-w-4xl": "max-width:56rem",
"max-w-full": "max-width:100%",
"max-w-0": "max-width:0",
"max-w-none": "max-width:none",
"max-w-screen-2xl": "max-width:1536px",
"max-w-[360px]": "max-width:360px",
"max-w-[768px]": "max-width:768px",
"max-w-[640px]": "max-width:640px",
"max-h-32": "max-height:8rem",
"max-h-64": "max-height:16rem",
"max-h-72": "max-height:18rem",
"max-h-96": "max-height:24rem",
"max-h-none": "max-height:none",
"max-h-[448px]": "max-height:448px",
"max-h-[50vh]": "max-height:50vh",
# ── Typography ───────────────────────────────────────────────────────
"text-xs": "font-size:.75rem;line-height:1rem",
"text-sm": "font-size:.875rem;line-height:1.25rem",
"text-base": "font-size:1rem;line-height:1.5rem",
"text-md": "font-size:1rem;line-height:1.5rem", # alias for text-base
"text-lg": "font-size:1.125rem;line-height:1.75rem",
"text-xl": "font-size:1.25rem;line-height:1.75rem",
"text-2xl": "font-size:1.5rem;line-height:2rem",
"text-3xl": "font-size:1.875rem;line-height:2.25rem",
"text-4xl": "font-size:2.25rem;line-height:2.5rem",
"text-5xl": "font-size:3rem;line-height:1",
"text-6xl": "font-size:3.75rem;line-height:1",
"text-8xl": "font-size:6rem;line-height:1",
"text-[8px]": "font-size:8px",
"text-[9px]": "font-size:9px",
"text-[10px]": "font-size:10px",
"text-[11px]": "font-size:11px",
"text-[13px]": "font-size:13px",
"text-[14px]": "font-size:14px",
"text-[16px]": "font-size:16px",
"text-[18px]": "font-size:18px",
"text-[36px]": "font-size:36px",
"text-[40px]": "font-size:40px",
"text-[0.6rem]": "font-size:.6rem",
"text-[0.65rem]": "font-size:.65rem",
"text-[0.7rem]": "font-size:.7rem",
"font-normal": "font-weight:400",
"font-medium": "font-weight:500",
"font-semibold": "font-weight:600",
"font-bold": "font-weight:700",
"font-mono": "font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace",
"italic": "font-style:italic",
"uppercase": "text-transform:uppercase",
"capitalize": "text-transform:capitalize",
"tabular-nums": "font-variant-numeric:tabular-nums",
"leading-none": "line-height:1",
"leading-tight": "line-height:1.25",
"leading-snug": "line-height:1.375",
"leading-relaxed": "line-height:1.625",
"tracking-tight": "letter-spacing:-.025em",
"tracking-wide": "letter-spacing:.025em",
"tracking-widest": "letter-spacing:.1em",
"text-left": "text-align:left",
"text-center": "text-align:center",
"text-right": "text-align:right",
"align-top": "vertical-align:top",
# ── Text Colors ──────────────────────────────────────────────────────
"text-white": "color:rgb(255 255 255)",
"text-white/80": "color:rgba(255,255,255,.8)",
"text-black": "color:rgb(0 0 0)",
"text-stone-300": "color:rgb(214 211 209)",
"text-stone-400": "color:rgb(168 162 158)",
"text-stone-500": "color:rgb(120 113 108)",
"text-stone-600": "color:rgb(87 83 78)",
"text-stone-700": "color:rgb(68 64 60)",
"text-stone-800": "color:rgb(41 37 36)",
"text-stone-900": "color:rgb(28 25 23)",
"text-slate-400": "color:rgb(148 163 184)",
"text-gray-500": "color:rgb(107 114 128)",
"text-gray-600": "color:rgb(75 85 99)",
"text-red-500": "color:rgb(239 68 68)",
"text-red-600": "color:rgb(220 38 38)",
"text-red-700": "color:rgb(185 28 28)",
"text-red-800": "color:rgb(153 27 27)",
"text-rose-500": "color:rgb(244 63 94)",
"text-rose-600": "color:rgb(225 29 72)",
"text-rose-700": "color:rgb(190 18 60)",
"text-rose-800": "color:rgb(159 18 57)",
"text-rose-800/80": "color:rgba(159,18,57,.8)",
"text-rose-900": "color:rgb(136 19 55)",
"text-orange-600": "color:rgb(234 88 12)",
"text-amber-500": "color:rgb(245 158 11)",
"text-amber-600": "color:rgb(217 119 6)",
"text-amber-700": "color:rgb(180 83 9)",
"text-amber-800": "color:rgb(146 64 14)",
"text-yellow-700": "color:rgb(161 98 7)",
"text-green-600": "color:rgb(22 163 74)",
"text-green-800": "color:rgb(22 101 52)",
"text-green-900": "color:rgb(20 83 45)",
"text-neutral-400": "color:rgb(163 163 163)",
"text-neutral-500": "color:rgb(115 115 115)",
"text-neutral-600": "color:rgb(82 82 82)",
"text-emerald-500": "color:rgb(16 185 129)",
"text-emerald-600": "color:rgb(5 150 105)",
"text-emerald-700": "color:rgb(4 120 87)",
"text-emerald-800": "color:rgb(6 95 70)",
"text-emerald-900": "color:rgb(6 78 59)",
"text-sky-600": "color:rgb(2 132 199)",
"text-sky-700": "color:rgb(3 105 161)",
"text-sky-800": "color:rgb(7 89 133)",
"text-blue-500": "color:rgb(59 130 246)",
"text-blue-600": "color:rgb(37 99 235)",
"text-blue-700": "color:rgb(29 78 216)",
"text-blue-800": "color:rgb(30 64 175)",
"text-purple-600": "color:rgb(147 51 234)",
"text-violet-600": "color:rgb(124 58 237)",
"text-violet-700": "color:rgb(109 40 217)",
"text-violet-800": "color:rgb(91 33 182)",
"text-violet-900": "color:rgb(76 29 149)",
# ── Background Colors ────────────────────────────────────────────────
"bg-transparent": "background-color:transparent",
"bg-white": "background-color:rgb(255 255 255)",
"bg-white/60": "background-color:rgba(255,255,255,.6)",
"bg-white/70": "background-color:rgba(255,255,255,.7)",
"bg-white/80": "background-color:rgba(255,255,255,.8)",
"bg-white/90": "background-color:rgba(255,255,255,.9)",
"bg-black": "background-color:rgb(0 0 0)",
"bg-black/50": "background-color:rgba(0,0,0,.5)",
"bg-stone-50": "background-color:rgb(250 250 249)",
"bg-stone-100": "background-color:rgb(245 245 244)",
"bg-stone-200": "background-color:rgb(231 229 228)",
"bg-stone-300": "background-color:rgb(214 211 209)",
"bg-stone-400": "background-color:rgb(168 162 158)",
"bg-stone-500": "background-color:rgb(120 113 108)",
"bg-stone-600": "background-color:rgb(87 83 78)",
"bg-stone-700": "background-color:rgb(68 64 60)",
"bg-stone-800": "background-color:rgb(41 37 36)",
"bg-stone-900": "background-color:rgb(28 25 23)",
"bg-slate-100": "background-color:rgb(241 245 249)",
"bg-slate-200": "background-color:rgb(226 232 240)",
"bg-gray-100": "background-color:rgb(243 244 246)",
"bg-red-50": "background-color:rgb(254 242 242)",
"bg-red-100": "background-color:rgb(254 226 226)",
"bg-red-200": "background-color:rgb(254 202 202)",
"bg-red-500": "background-color:rgb(239 68 68)",
"bg-red-600": "background-color:rgb(220 38 38)",
"bg-rose-50": "background-color:rgb(255 241 242)",
"bg-rose-50/80": "background-color:rgba(255,241,242,.8)",
"bg-orange-100": "background-color:rgb(255 237 213)",
"bg-amber-50": "background-color:rgb(255 251 235)",
"bg-amber-50/60": "background-color:rgba(255,251,235,.6)",
"bg-amber-100": "background-color:rgb(254 243 199)",
"bg-amber-500": "background-color:rgb(245 158 11)",
"bg-amber-600": "background-color:rgb(217 119 6)",
"bg-yellow-50": "background-color:rgb(254 252 232)",
"bg-yellow-100": "background-color:rgb(254 249 195)",
"bg-yellow-200": "background-color:rgb(254 240 138)",
"bg-yellow-300": "background-color:rgb(253 224 71)",
"bg-green-50": "background-color:rgb(240 253 244)",
"bg-green-100": "background-color:rgb(220 252 231)",
"bg-green-200": "background-color:rgb(187 247 208)",
"bg-neutral-50/70": "background-color:rgba(250,250,250,.7)",
"bg-black/70": "background-color:rgba(0,0,0,.7)",
"bg-emerald-50": "background-color:rgb(236 253 245)",
"bg-emerald-50/80": "background-color:rgba(236,253,245,.8)",
"bg-emerald-100": "background-color:rgb(209 250 229)",
"bg-emerald-200": "background-color:rgb(167 243 208)",
"bg-emerald-500": "background-color:rgb(16 185 129)",
"bg-emerald-600": "background-color:rgb(5 150 105)",
"bg-sky-100": "background-color:rgb(224 242 254)",
"bg-sky-200": "background-color:rgb(186 230 253)",
"bg-sky-300": "background-color:rgb(125 211 252)",
"bg-sky-400": "background-color:rgb(56 189 248)",
"bg-sky-500": "background-color:rgb(14 165 233)",
"bg-blue-50": "background-color:rgb(239 246 255)",
"bg-blue-100": "background-color:rgb(219 234 254)",
"bg-blue-600": "background-color:rgb(37 99 235)",
"bg-purple-600": "background-color:rgb(147 51 234)",
"bg-violet-50": "background-color:rgb(245 243 255)",
"bg-violet-100": "background-color:rgb(237 233 254)",
"bg-violet-200": "background-color:rgb(221 214 254)",
"bg-violet-300": "background-color:rgb(196 181 253)",
"bg-violet-400": "background-color:rgb(167 139 250)",
"bg-violet-500": "background-color:rgb(139 92 246)",
"bg-violet-600": "background-color:rgb(124 58 237)",
"bg-violet-700": "background-color:rgb(109 40 217)",
"bg-amber-200": "background-color:rgb(253 230 138)",
"bg-blue-700": "background-color:rgb(29 78 216)",
"bg-emerald-700": "background-color:rgb(4 120 87)",
"bg-purple-700": "background-color:rgb(126 34 206)",
"bg-stone-50/60": "background-color:rgba(250,250,249,.6)",
# ── Border ───────────────────────────────────────────────────────────
"border": "border-width:1px",
"border-2": "border-width:2px",
"border-4": "border-width:4px",
"border-t": "border-top-width:1px",
"border-t-0": "border-top-width:0",
"border-b": "border-bottom-width:1px",
"border-b-2": "border-bottom-width:2px",
"border-r": "border-right-width:1px",
"border-l": "border-left-width:1px",
"border-l-4": "border-left-width:4px",
"border-dashed": "border-style:dashed",
"border-none": "border-style:none",
"border-transparent": "border-color:transparent",
"border-white": "border-color:rgb(255 255 255)",
"border-white/30": "border-color:rgba(255,255,255,.3)",
"border-stone-100": "border-color:rgb(245 245 244)",
"border-stone-200": "border-color:rgb(231 229 228)",
"border-stone-300": "border-color:rgb(214 211 209)",
"border-stone-700": "border-color:rgb(68 64 60)",
"border-red-200": "border-color:rgb(254 202 202)",
"border-red-300": "border-color:rgb(252 165 165)",
"border-rose-200": "border-color:rgb(254 205 211)",
"border-rose-300": "border-color:rgb(253 164 175)",
"border-amber-200": "border-color:rgb(253 230 138)",
"border-amber-300": "border-color:rgb(252 211 77)",
"border-yellow-200": "border-color:rgb(254 240 138)",
"border-green-300": "border-color:rgb(134 239 172)",
"border-emerald-100": "border-color:rgb(209 250 229)",
"border-emerald-200": "border-color:rgb(167 243 208)",
"border-emerald-300": "border-color:rgb(110 231 183)",
"border-emerald-600": "border-color:rgb(5 150 105)",
"border-blue-200": "border-color:rgb(191 219 254)",
"border-blue-300": "border-color:rgb(147 197 253)",
"border-violet-200": "border-color:rgb(221 214 254)",
"border-violet-300": "border-color:rgb(196 181 253)",
"border-violet-400": "border-color:rgb(167 139 250)",
"border-neutral-200": "border-color:rgb(229 229 229)",
"border-red-400": "border-color:rgb(248 113 113)",
"border-stone-400": "border-color:rgb(168 162 158)",
"border-t-white": "border-top-color:rgb(255 255 255)",
"border-t-stone-600": "border-top-color:rgb(87 83 78)",
"border-l-stone-400": "border-left-color:rgb(168 162 158)",
# ── Border Radius ────────────────────────────────────────────────────
"rounded": "border-radius:.25rem",
"rounded-md": "border-radius:.375rem",
"rounded-lg": "border-radius:.5rem",
"rounded-xl": "border-radius:.75rem",
"rounded-2xl": "border-radius:1rem",
"rounded-full": "border-radius:9999px",
"rounded-t": "border-top-left-radius:.25rem;border-top-right-radius:.25rem",
"rounded-b": "border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem",
"rounded-[4px]": "border-radius:4px",
"rounded-[8px]": "border-radius:8px",
# ── Shadow ───────────────────────────────────────────────────────────
"shadow-sm": "box-shadow:0 1px 2px 0 rgba(0,0,0,.05)",
"shadow": "box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1)",
"shadow-md": "box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)",
"shadow-lg": "box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1)",
"shadow-xl": "box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1)",
# ── Opacity ──────────────────────────────────────────────────────────
"opacity-0": "opacity:0",
"opacity-40": "opacity:.4",
"opacity-50": "opacity:.5",
"opacity-90": "opacity:.9",
"opacity-100": "opacity:1",
# ── Ring / Outline ───────────────────────────────────────────────────
"outline-none": "outline:2px solid transparent;outline-offset:2px",
"ring-2": "box-shadow:0 0 0 2px var(--tw-ring-color,rgb(59 130 246))",
"ring-offset-2": "box-shadow:0 0 0 2px rgb(255 255 255),0 0 0 4px var(--tw-ring-color,rgb(59 130 246))",
"ring-stone-300": "--tw-ring-color:rgb(214 211 209)",
"ring-stone-500": "--tw-ring-color:rgb(120 113 108)",
"ring-violet-500": "--tw-ring-color:rgb(139 92 246)",
"ring-blue-500": "--tw-ring-color:rgb(59 130 246)",
"ring-green-500": "--tw-ring-color:rgb(22 163 74)",
"ring-purple-500": "--tw-ring-color:rgb(147 51 234)",
# ── Overflow ─────────────────────────────────────────────────────────
"overflow-hidden": "overflow:hidden",
"overflow-x-auto": "overflow-x:auto",
"overflow-y-auto": "overflow-y:auto",
"overflow-visible": "overflow:visible",
"overflow-y-visible": "overflow-y:visible",
"overscroll-contain": "overscroll-behavior:contain",
# ── Text Decoration ──────────────────────────────────────────────────
"underline": "text-decoration-line:underline",
"line-through": "text-decoration-line:line-through",
"no-underline": "text-decoration-line:none",
# ── Text Overflow ────────────────────────────────────────────────────
"truncate": "overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
"line-clamp-2": "display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden",
"line-clamp-3": "display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden",
# ── Whitespace / Word Break ──────────────────────────────────────────
"whitespace-normal": "white-space:normal",
"whitespace-nowrap": "white-space:nowrap",
"whitespace-pre-line": "white-space:pre-line",
"whitespace-pre-wrap": "white-space:pre-wrap",
"break-words": "overflow-wrap:break-word",
"break-all": "word-break:break-all",
# ── Transform ────────────────────────────────────────────────────────
"rotate-180": "transform:rotate(180deg)",
"-translate-x-1/2": "transform:translateX(-50%)",
"-translate-y-1/2": "transform:translateY(-50%)",
# ── Transition ───────────────────────────────────────────────────────
"transition": "transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
"transition-all": "transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
"transition-colors": "transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
"transition-opacity": "transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
"transition-transform": "transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s",
"duration-75": "transition-duration:75ms",
"duration-100": "transition-duration:100ms",
"duration-150": "transition-duration:150ms",
"duration-200": "transition-duration:200ms",
"duration-300": "transition-duration:300ms",
"duration-500": "transition-duration:500ms",
"duration-700": "transition-duration:700ms",
# ── Animation ────────────────────────────────────────────────────────
"animate-spin": "animation:spin 1s linear infinite",
"animate-ping": "animation:ping 1s cubic-bezier(0,0,0.2,1) infinite",
"animate-pulse": "animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
"animate-bounce": "animation:bounce 1s infinite",
"animate-none": "animation:none",
# ── Aspect Ratio ─────────────────────────────────────────────────────
"aspect-square": "aspect-ratio:1/1",
"aspect-video": "aspect-ratio:16/9",
# ── Object Fit / Position ────────────────────────────────────────────
"object-contain": "object-fit:contain",
"object-cover": "object-fit:cover",
"object-center": "object-position:center",
"object-top": "object-position:top",
# ── Cursor ───────────────────────────────────────────────────────────
"cursor-pointer": "cursor:pointer",
"cursor-move": "cursor:move",
# ── User Select ──────────────────────────────────────────────────────
"select-none": "user-select:none",
"select-all": "user-select:all",
# ── Pointer Events ───────────────────────────────────────────────────
"pointer-events-none": "pointer-events:none",
# ── Resize ───────────────────────────────────────────────────────────
"resize": "resize:both",
"resize-none": "resize:none",
# ── Scroll Snap ──────────────────────────────────────────────────────
"snap-y": "scroll-snap-type:y mandatory",
"snap-start": "scroll-snap-align:start",
"snap-mandatory": "scroll-snap-type:y mandatory",
# ── List Style ───────────────────────────────────────────────────────
"list-disc": "list-style-type:disc",
"list-decimal": "list-style-type:decimal",
"list-inside": "list-style-position:inside",
# ── Table ────────────────────────────────────────────────────────────
"table-fixed": "table-layout:fixed",
# ── Backdrop ─────────────────────────────────────────────────────────
"backdrop-blur": "backdrop-filter:blur(8px)",
"backdrop-blur-sm": "backdrop-filter:blur(4px)",
"backdrop-blur-md": "backdrop-filter:blur(12px)",
# ── Filter ───────────────────────────────────────────────────────────
"saturate-0": "filter:saturate(0)",
# ── Space Between (child selector atoms) ─────────────────────────────
# These generate `.atom > :not(:first-child)` rules
"space-y-0": "margin-top:0",
"space-y-0.5": "margin-top:.125rem",
"space-y-1": "margin-top:.25rem",
"space-y-2": "margin-top:.5rem",
"space-y-3": "margin-top:.75rem",
"space-y-4": "margin-top:1rem",
"space-y-6": "margin-top:1.5rem",
"space-y-8": "margin-top:2rem",
"space-y-10": "margin-top:2.5rem",
"space-x-1": "margin-left:.25rem",
"space-x-2": "margin-left:.5rem",
# ── Divide (child selector atoms) ────────────────────────────────────
# These generate `.atom > :not(:first-child)` rules
"divide-y": "border-top-width:1px",
"divide-stone-100": "border-color:rgb(245 245 244)",
"divide-stone-200": "border-color:rgb(231 229 228)",
# ── Important modifiers ──────────────────────────────────────────────
"!bg-stone-500": "background-color:rgb(120 113 108)!important",
"!text-white": "color:rgb(255 255 255)!important",
}
# Atoms that need a child selector: `.atom > :not(:first-child)` instead of `.atom`
CHILD_SELECTOR_ATOMS: frozenset[str] = frozenset({
k for k in STYLE_ATOMS
if k.startswith(("space-x-", "space-y-", "divide-y", "divide-x"))
and not k.startswith("divide-stone")
})
# ═══════════════════════════════════════════════════════════════════════════
# Pseudo-class / pseudo-element variants
# ═══════════════════════════════════════════════════════════════════════════
PSEUDO_VARIANTS: dict[str, str] = {
"hover": ":hover",
"focus": ":focus",
"focus-within": ":focus-within",
"focus-visible": ":focus-visible",
"active": ":active",
"disabled": ":disabled",
"first": ":first-child",
"last": ":last-child",
"odd": ":nth-child(odd)",
"even": ":nth-child(even)",
"empty": ":empty",
"open": "[open]",
"placeholder": "::placeholder",
"file": "::file-selector-button",
"aria-selected": "[aria-selected=true]",
"invalid": ":invalid",
"placeholder-shown": ":placeholder-shown",
"group-hover": ":is(.group:hover) &",
"group-open": ":is(.group[open]) &",
"group-open/cat": ":is(.group\\/cat[open]) &",
"group-open/filter": ":is(.group\\/filter[open]) &",
"group-open/root": ":is(.group\\/root[open]) &",
}
# ═══════════════════════════════════════════════════════════════════════════
# Responsive breakpoints
# ═══════════════════════════════════════════════════════════════════════════
RESPONSIVE_BREAKPOINTS: dict[str, str] = {
"sm": "(min-width:640px)",
"md": "(min-width:768px)",
"lg": "(min-width:1024px)",
"xl": "(min-width:1280px)",
"2xl": "(min-width:1536px)",
}
# ═══════════════════════════════════════════════════════════════════════════
# Keyframes — built-in animation definitions
# ═══════════════════════════════════════════════════════════════════════════
KEYFRAMES: dict[str, str] = {
"spin": "@keyframes spin{to{transform:rotate(360deg)}}",
"ping": "@keyframes ping{75%,100%{transform:scale(2);opacity:0}}",
"pulse": "@keyframes pulse{50%{opacity:.5}}",
"bounce": "@keyframes bounce{0%,100%{transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,0.2,1)}}",
}
# ═══════════════════════════════════════════════════════════════════════════
# Arbitrary value patterns — fallback when atom not in STYLE_ATOMS
# ═══════════════════════════════════════════════════════════════════════════
#
# Each tuple is (regex_pattern, css_template).
# The regex captures value groups; the template uses {0}, {1}, etc.
ARBITRARY_PATTERNS: list[tuple[str, str]] = [
# Width / Height
(r"w-\[(.+)\]", "width:{0}"),
(r"h-\[(.+)\]", "height:{0}"),
(r"min-w-\[(.+)\]", "min-width:{0}"),
(r"min-h-\[(.+)\]", "min-height:{0}"),
(r"max-w-\[(.+)\]", "max-width:{0}"),
(r"max-h-\[(.+)\]", "max-height:{0}"),
# Spacing
(r"p-\[(.+)\]", "padding:{0}"),
(r"px-\[(.+)\]", "padding-left:{0};padding-right:{0}"),
(r"py-\[(.+)\]", "padding-top:{0};padding-bottom:{0}"),
(r"pt-\[(.+)\]", "padding-top:{0}"),
(r"pb-\[(.+)\]", "padding-bottom:{0}"),
(r"pl-\[(.+)\]", "padding-left:{0}"),
(r"pr-\[(.+)\]", "padding-right:{0}"),
(r"m-\[(.+)\]", "margin:{0}"),
(r"mx-\[(.+)\]", "margin-left:{0};margin-right:{0}"),
(r"my-\[(.+)\]", "margin-top:{0};margin-bottom:{0}"),
(r"mt-\[(.+)\]", "margin-top:{0}"),
(r"mb-\[(.+)\]", "margin-bottom:{0}"),
(r"ml-\[(.+)\]", "margin-left:{0}"),
(r"mr-\[(.+)\]", "margin-right:{0}"),
# Gap
(r"gap-\[(.+)\]", "gap:{0}"),
(r"gap-x-\[(.+)\]", "column-gap:{0}"),
(r"gap-y-\[(.+)\]", "row-gap:{0}"),
# Position
(r"top-\[(.+)\]", "top:{0}"),
(r"right-\[(.+)\]", "right:{0}"),
(r"bottom-\[(.+)\]", "bottom:{0}"),
(r"left-\[(.+)\]", "left:{0}"),
# Border radius
(r"rounded-\[(.+)\]", "border-radius:{0}"),
# Background / Text color
(r"bg-\[(.+)\]", "background-color:{0}"),
(r"text-\[(.+)\]", "font-size:{0}"),
# Grid
(r"grid-cols-\[(.+)\]", "grid-template-columns:{0}"),
(r"col-span-(\d+)", "grid-column:span {0}/span {0}"),
]

View File

@@ -1,254 +0,0 @@
"""
Style resolver — ``(css :flex :gap-4 :hover:bg-sky-200)`` → StyleValue.
Resolves a tuple of atom strings into a ``StyleValue`` with:
- A content-addressed class name (``sx-{hash[:6]}``)
- Base CSS declarations
- Pseudo-class rules (hover, focus, etc.)
- Media-query rules (responsive breakpoints)
- Referenced @keyframes definitions
Resolution order per atom:
1. Dictionary lookup in ``STYLE_ATOMS``
2. Arbitrary value pattern match (``w-[347px]`` → ``width:347px``)
3. Ignored (unknown atoms are silently skipped)
Results are memoized by input tuple for zero-cost repeat calls.
"""
from __future__ import annotations
import hashlib
import re
from functools import lru_cache
from typing import Sequence
from .style_dict import (
ARBITRARY_PATTERNS,
CHILD_SELECTOR_ATOMS,
KEYFRAMES,
PSEUDO_VARIANTS,
RESPONSIVE_BREAKPOINTS,
STYLE_ATOMS,
)
from .types import StyleValue
# ---------------------------------------------------------------------------
# Compiled arbitrary-value patterns
# ---------------------------------------------------------------------------
_COMPILED_PATTERNS: list[tuple[re.Pattern, str]] = [
(re.compile(f"^{pat}$"), tmpl)
for pat, tmpl in ARBITRARY_PATTERNS
]
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def resolve_style(atoms: tuple[str, ...]) -> StyleValue:
"""Resolve a tuple of keyword atoms into a StyleValue.
Each atom is a Tailwind-compatible keyword (``flex``, ``gap-4``,
``hover:bg-sky-200``, ``sm:flex-row``, etc.). Both keywords
(without leading colon) and runtime strings are accepted.
"""
return _resolve_cached(atoms)
def merge_styles(styles: Sequence[StyleValue]) -> StyleValue:
"""Merge multiple StyleValues into one.
Later declarations win for the same CSS property. Class name is
recomputed from the merged declarations.
"""
if len(styles) == 1:
return styles[0]
all_decls: list[str] = []
all_media: list[tuple[str, str]] = []
all_pseudo: list[tuple[str, str]] = []
all_kf: list[tuple[str, str]] = []
for sv in styles:
if sv.declarations:
all_decls.append(sv.declarations)
all_media.extend(sv.media_rules)
all_pseudo.extend(sv.pseudo_rules)
all_kf.extend(sv.keyframes)
merged_decls = ";".join(all_decls)
return _build_style_value(
merged_decls,
tuple(all_media),
tuple(all_pseudo),
tuple(dict(all_kf).items()), # dedupe keyframes by name
)
# ---------------------------------------------------------------------------
# Internal resolution
# ---------------------------------------------------------------------------
@lru_cache(maxsize=4096)
def _resolve_cached(atoms: tuple[str, ...]) -> StyleValue:
"""Memoized resolver."""
base_decls: list[str] = []
media_rules: list[tuple[str, str]] = [] # (query, decls)
pseudo_rules: list[tuple[str, str]] = [] # (selector_suffix, decls)
keyframes_needed: list[tuple[str, str]] = []
for atom in atoms:
if not atom:
continue
# Strip leading colon if keyword form (":flex" → "flex")
a = atom.lstrip(":")
# Split variant prefix(es): "hover:bg-sky-200" → ["hover", "bg-sky-200"]
# "sm:hover:bg-sky-200" → ["sm", "hover", "bg-sky-200"]
variant, base = _split_variant(a)
# Resolve the base atom to CSS declarations
decls = _resolve_atom(base)
if not decls:
continue
# Check if this atom references a keyframe
_check_keyframes(base, keyframes_needed)
# Route to the appropriate bucket
if variant is None:
base_decls.append(decls)
elif variant in RESPONSIVE_BREAKPOINTS:
query = RESPONSIVE_BREAKPOINTS[variant]
media_rules.append((query, decls))
elif variant in PSEUDO_VARIANTS:
pseudo_sel = PSEUDO_VARIANTS[variant]
pseudo_rules.append((pseudo_sel, decls))
else:
# Compound variant: "sm:hover:..." → media + pseudo
parts = variant.split(":")
media_part = None
pseudo_part = None
for p in parts:
if p in RESPONSIVE_BREAKPOINTS:
media_part = RESPONSIVE_BREAKPOINTS[p]
elif p in PSEUDO_VARIANTS:
pseudo_part = PSEUDO_VARIANTS[p]
if media_part and pseudo_part:
# Both media and pseudo — store as pseudo within media
# For now, put in pseudo_rules with media annotation
pseudo_rules.append((pseudo_part, decls))
media_rules.append((media_part, decls))
elif media_part:
media_rules.append((media_part, decls))
elif pseudo_part:
pseudo_rules.append((pseudo_part, decls))
else:
# Unknown variant — treat as base
base_decls.append(decls)
return _build_style_value(
";".join(base_decls),
tuple(media_rules),
tuple(pseudo_rules),
tuple(keyframes_needed),
)
def _split_variant(atom: str) -> tuple[str | None, str]:
"""Split a potentially variant-prefixed atom.
Returns (variant, base) where variant is None for non-prefixed atoms.
Examples:
"flex" → (None, "flex")
"hover:bg-sky-200" → ("hover", "bg-sky-200")
"sm:flex-row" → ("sm", "flex-row")
"sm:hover:bg-sky-200" → ("sm:hover", "bg-sky-200")
"""
# Check for responsive prefix first (always outermost)
for bp in RESPONSIVE_BREAKPOINTS:
prefix = bp + ":"
if atom.startswith(prefix):
rest = atom[len(prefix):]
# Check for nested pseudo variant
for pv in PSEUDO_VARIANTS:
inner_prefix = pv + ":"
if rest.startswith(inner_prefix):
return (bp + ":" + pv, rest[len(inner_prefix):])
return (bp, rest)
# Check for pseudo variant
for pv in PSEUDO_VARIANTS:
prefix = pv + ":"
if atom.startswith(prefix):
return (pv, atom[len(prefix):])
return (None, atom)
def _resolve_atom(atom: str) -> str | None:
"""Look up CSS declarations for a single base atom.
Returns None if the atom is unknown.
"""
# 1. Dictionary lookup
decls = STYLE_ATOMS.get(atom)
if decls is not None:
return decls
# 2. Dynamic keyframes: animate-{name} → animation-name:{name}
if atom.startswith("animate-"):
name = atom[len("animate-"):]
if name in KEYFRAMES:
return f"animation-name:{name}"
# 3. Arbitrary value pattern match
for pattern, template in _COMPILED_PATTERNS:
m = pattern.match(atom)
if m:
groups = m.groups()
result = template
for i, g in enumerate(groups):
result = result.replace(f"{{{i}}}", g)
return result
# 4. Unknown atom — silently skip
return None
def _check_keyframes(atom: str, kf_list: list[tuple[str, str]]) -> None:
"""If the atom references a built-in animation, add its @keyframes."""
if atom.startswith("animate-"):
name = atom[len("animate-"):]
if name in KEYFRAMES:
kf_list.append((name, KEYFRAMES[name]))
def _build_style_value(
declarations: str,
media_rules: tuple,
pseudo_rules: tuple,
keyframes: tuple,
) -> StyleValue:
"""Build a StyleValue with a content-addressed class name."""
# Build hash from all rules for deterministic class name
hash_input = declarations
for query, decls in media_rules:
hash_input += f"@{query}{{{decls}}}"
for sel, decls in pseudo_rules:
hash_input += f"{sel}{{{decls}}}"
for name, rule in keyframes:
hash_input += rule
h = hashlib.sha256(hash_input.encode()).hexdigest()[:6]
class_name = f"sx-{h}"
return StyleValue(
class_name=class_name,
declarations=declarations,
media_rules=media_rules,
pseudo_rules=pseudo_rules,
keyframes=keyframes,
)

View File

@@ -303,30 +303,6 @@ class ActionDef:
return f"<action:{self.name}({', '.join(self.params)})>"
# ---------------------------------------------------------------------------
# StyleValue
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class StyleValue:
"""A resolved CSS style produced by ``(css :flex :gap-4 :hover:bg-sky-200)``.
Generated by the style resolver. The renderer emits ``class_name`` as a
CSS class and registers the CSS rule for on-demand delivery.
"""
class_name: str # "sx-a3f2c1"
declarations: str # "display:flex;gap:1rem"
media_rules: tuple = () # ((query, decls), ...)
pseudo_rules: tuple = () # ((selector, decls), ...)
keyframes: tuple = () # (("spin", "@keyframes spin{...}"), ...)
def __repr__(self):
return f"<StyleValue {self.class_name}>"
def __str__(self):
return self.class_name
# ---------------------------------------------------------------------------
# Continuation
# ---------------------------------------------------------------------------
@@ -368,4 +344,4 @@ class _ShiftSignal(BaseException):
# ---------------------------------------------------------------------------
# An s-expression value after evaluation
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None
SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | list | dict | _Nil | None