Add on-demand CSS: registry, pre-computed component classes, header compression
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 42s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 42s
- Parse tw.css into per-class lookup registry at startup
- Pre-scan component CSS classes at registration time (avoid per-request regex)
- Compress SX-Css header: 8-char hash replaces full class list (LRU cache)
- Add ;@css comment annotation for dynamically constructed class names
- Safelist bg-sky-{100..400} in Tailwind config for menu-row-sx dynamic shades
- Client sends/receives hash, falls back gracefully on cache miss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,34 @@ from .page import SEARCH_HEADERS_MOBILE, SEARCH_HEADERS_DESKTOP
|
||||
from .parser import SxExpr
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-computed CSS classes for inline sx built by Python helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
# These :class strings appear in post_header_sx / post_admin_header_sx etc.
|
||||
# They're static — scan once at import time so they aren't re-scanned per request.
|
||||
|
||||
_HELPER_CLASS_SOURCES = [
|
||||
':class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"',
|
||||
':class "relative nav-group"',
|
||||
':class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"',
|
||||
':class "!bg-stone-500 !text-white"',
|
||||
':class "fa fa-cog"',
|
||||
':class "fa fa-shield-halved"',
|
||||
':class "text-white"',
|
||||
':class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded !bg-stone-500 !text-white p-3"',
|
||||
]
|
||||
|
||||
|
||||
def _scan_helper_classes() -> frozenset[str]:
|
||||
"""Scan the static class strings from helper functions once."""
|
||||
from .css_registry import scan_classes_from_sx
|
||||
combined = " ".join(_HELPER_CLASS_SOURCES)
|
||||
return frozenset(scan_classes_from_sx(combined))
|
||||
|
||||
|
||||
HELPER_CSS_CLASSES: frozenset[str] = _scan_helper_classes()
|
||||
|
||||
|
||||
def call_url(ctx: dict, key: str, path: str = "/") -> str:
|
||||
"""Call a URL helper from context (e.g., blog_url, account_url)."""
|
||||
fn = ctx.get(key)
|
||||
@@ -354,16 +382,57 @@ def sx_response(source_or_component: str, status: int = 200,
|
||||
logging.getLogger("sx").error("sx_response parse error: %s\nSource (first 500): %s", _e, source[:500])
|
||||
|
||||
# For SX requests, prepend missing component definitions
|
||||
comp_defs = ""
|
||||
if request.headers.get("SX-Request"):
|
||||
comp_defs = components_for_request()
|
||||
if comp_defs:
|
||||
body = (f'<script type="text/sx" data-components>'
|
||||
f'{comp_defs}</script>\n{body}')
|
||||
|
||||
# On-demand CSS: scan source for classes, send only new rules
|
||||
from .css_registry import scan_classes_from_sx, lookup_rules, registry_loaded, lookup_css_hash, store_css_hash
|
||||
from .jinja_bridge import _COMPONENT_ENV
|
||||
from .types import Component as _Component
|
||||
new_classes: set[str] = set()
|
||||
cumulative_classes: set[str] = set()
|
||||
if registry_loaded():
|
||||
new_classes = scan_classes_from_sx(source)
|
||||
# Include pre-computed helper classes (menu bars, admin nav, etc.)
|
||||
new_classes.update(HELPER_CSS_CLASSES)
|
||||
if comp_defs:
|
||||
# Use pre-computed classes for components being sent
|
||||
for key, val in _COMPONENT_ENV.items():
|
||||
if isinstance(val, _Component) and val.css_classes:
|
||||
new_classes.update(val.css_classes)
|
||||
|
||||
# Resolve known classes from SX-Css header (hash or full list)
|
||||
known_classes: set[str] = set()
|
||||
known_raw = request.headers.get("SX-Css", "")
|
||||
if known_raw:
|
||||
if len(known_raw) <= 16:
|
||||
# Treat as hash
|
||||
looked_up = lookup_css_hash(known_raw)
|
||||
if looked_up is not None:
|
||||
known_classes = looked_up
|
||||
else:
|
||||
# Cache miss — send all classes (safe fallback)
|
||||
known_classes = set()
|
||||
else:
|
||||
known_classes = set(known_raw.split(","))
|
||||
|
||||
cumulative_classes = known_classes | new_classes
|
||||
new_classes -= known_classes
|
||||
|
||||
if new_classes:
|
||||
new_rules = lookup_rules(new_classes)
|
||||
if new_rules:
|
||||
body = f'<style data-sx-css>{new_rules}</style>\n{body}'
|
||||
|
||||
resp = Response(body, status=status, content_type="text/sx")
|
||||
resp.headers["X-SX-Body-Len"] = str(len(body))
|
||||
resp.headers["X-SX-Source-Len"] = str(len(source))
|
||||
resp.headers["X-SX-Has-Defs"] = "1" if "<script" in body else "0"
|
||||
if new_classes:
|
||||
resp.headers["SX-Css-Add"] = ",".join(sorted(new_classes))
|
||||
if cumulative_classes:
|
||||
resp.headers["SX-Css-Hash"] = store_css_hash(cumulative_classes)
|
||||
if headers:
|
||||
for k, v in headers.items():
|
||||
resp.headers[k] = v
|
||||
@@ -386,14 +455,9 @@ _SX_PAGE_TEMPLATE = """\
|
||||
<title>{title}</title>
|
||||
{meta_html}
|
||||
<style>@media (min-width: 768px) {{ .js-mobile-sentinel {{ display:none !important; }} }}</style>
|
||||
<link rel="stylesheet" type="text/css" href="{asset_url}/styles/basics.css">
|
||||
<link rel="stylesheet" type="text/css" href="{asset_url}/styles/cards.css">
|
||||
<link rel="stylesheet" type="text/css" href="{asset_url}/styles/blog-content.css">
|
||||
<meta name="csrf-token" content="{csrf}">
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<link rel="stylesheet" href="{asset_url}/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="{asset_url}/fontawesome/css/v4-shims.min.css">
|
||||
<link href="https://unpkg.com/prismjs/themes/prism.css" rel="stylesheet">
|
||||
<style id="sx-css">{sx_css}</style>
|
||||
<meta name="sx-css-classes" content="{sx_css_classes}">
|
||||
<script src="https://unpkg.com/prismjs/prism.js"></script>
|
||||
<script src="https://unpkg.com/prismjs/components/prism-javascript.min.js"></script>
|
||||
<script src="https://unpkg.com/prismjs/components/prism-python.min.js"></script>
|
||||
@@ -418,7 +482,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
|
||||
<body class="bg-stone-50 text-stone-900">
|
||||
<script type="text/sx" data-components>{component_defs}</script>
|
||||
<script type="text/sx" data-mount="body">{page_sx}</script>
|
||||
<script src="{asset_url}/scripts/sx.js?v=20260301b"></script>
|
||||
<script src="{asset_url}/scripts/sx.js?v=20260301c"></script>
|
||||
<script src="{asset_url}/scripts/body.js"></script>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -429,9 +493,13 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
"""Return a minimal HTML shell that boots the page from sx source.
|
||||
|
||||
The browser loads component definitions and page sx, then sx.js
|
||||
renders everything client-side.
|
||||
renders everything client-side. CSS rules are scanned from the sx
|
||||
source and component defs, then injected as a <style> block.
|
||||
"""
|
||||
from .jinja_bridge import client_components_tag
|
||||
from .jinja_bridge import client_components_tag, _COMPONENT_ENV
|
||||
from .css_registry import scan_classes_from_sx, lookup_rules, get_preamble, registry_loaded, store_css_hash
|
||||
from .types import Component
|
||||
|
||||
components_tag = client_components_tag()
|
||||
# Extract just the inner source from the <script> tag
|
||||
component_defs = ""
|
||||
@@ -442,6 +510,27 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
if start > 0 and end > start:
|
||||
component_defs = components_tag[start:end]
|
||||
|
||||
# Scan for CSS classes — use pre-computed sets for components, scan page sx at request time
|
||||
sx_css = ""
|
||||
sx_css_classes = ""
|
||||
sx_css_hash = ""
|
||||
if registry_loaded():
|
||||
# Union pre-computed component classes instead of re-scanning source
|
||||
classes: set[str] = set()
|
||||
for val in _COMPONENT_ENV.values():
|
||||
if isinstance(val, Component) and val.css_classes:
|
||||
classes.update(val.css_classes)
|
||||
# Include pre-computed helper classes (menu bars, admin nav, etc.)
|
||||
classes.update(HELPER_CSS_CLASSES)
|
||||
# Page sx is unique per request — scan it
|
||||
classes.update(scan_classes_from_sx(page_sx))
|
||||
# Always include body classes
|
||||
classes.update(["bg-stone-50", "text-stone-900"])
|
||||
rules = lookup_rules(classes)
|
||||
sx_css = get_preamble() + rules
|
||||
sx_css_hash = store_css_hash(classes)
|
||||
sx_css_classes = sx_css_hash
|
||||
|
||||
asset_url = get_asset_url(ctx)
|
||||
title = ctx.get("base_title", "Rose Ash")
|
||||
csrf = _get_csrf_token()
|
||||
@@ -453,6 +542,8 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
csrf=_html_escape(csrf),
|
||||
component_defs=component_defs,
|
||||
page_sx=page_sx,
|
||||
sx_css=sx_css,
|
||||
sx_css_classes=sx_css_classes,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user