"""Validated per-device static CSS for pages and nested layout grids. Uses exact IDs from validate_blocks, never user supplied CSS selectors/expressions. Desktop structure is preserved; tablet and mobile may have distinct spans/order. """ import re,json,hashlib from .blueprints import validate_blocks _ID=re.compile(r'[a-zA-Z0-9_-]{4,50}\Z') def editor_css(blocks,prefix=""): tablet=[];mobile=[] def walk(items): for b in items: uid=b['id'] if not _ID.fullmatch(uid):continue s=b.get('style') or {} tp=s.get('tablet_padding',50);mp=s.get('mobile_padding',28) tp=max(0,min(120,int(tp)));mp=max(0,min(120,int(mp))) ident=prefix+f'.sf-section[data-block="{uid}"]' tablet.append(f'{ident}{{--sf-pad:{tp}px}}') mobile.append(f'{ident}{{--sf-pad:{mp}px}}') if s.get('hide_tablet') is True:tablet.append(f'{ident}{{display:none!important}}') if s.get('hide_mobile') is True:mobile.append(f'{ident}{{display:none!important}}') if b['type']=='layout': selector=prefix+f'.sf-layout-grid[data-layout="{uid}"]' tablet.append(f'{selector}{{grid-template-columns:repeat({b["tablet_columns"]},minmax(0,1fr))!important}}') mobile.append(f'{selector}{{grid-template-columns:repeat({b["mobile_columns"]},minmax(0,1fr))!important}}') for ci,col in enumerate(b['columns'],1): target=selector+f'>.sf-layout-column[data-col="{ci}"]' for dev,rules in [('tablet',tablet),('mobile',mobile)]: rules.append(f'{target}{{order:{col[dev+"_order"]};grid-column:span {col[dev+"_span"]}}}') if col['hide_'+dev]:rules.append(f'{target}{{display:none!important}}') walk(col['blocks']) walk(validate_blocks(blocks)) # Breakpoints are intentionally exclusive: phone has tablet rules plus # phone overrides. Phone controls are appended after tablet controls. return '@media(max-width:820px){'+''.join(tablet)+'}@media(max-width:540px){'+''.join(mobile)+'}' def route_key(route): return hashlib.sha256(str(route).encode()).hexdigest()[:16] def site_css(entries): styles=[] for entry in entries: try:meta=json.loads(entry.get('meta_json') or '{}') except (TypeError,ValueError):continue if isinstance(meta.get('blocks'),list) and meta['blocks']: styles.append(editor_css(meta['blocks'],prefix=f'body[data-siteforge-route="{route_key(entry.get("route","/"))}"] ')) # Identical generated IDs may occur on different pages. Styles are scoped # to block IDs and remain deterministic; validate each page independently. return ''.join(styles)