8cea7dee20
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests) - Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR imports keep post/product types instead of collapsing to page - Docs: trade-pro theme port spec (R3) and implementation plan Co-Authored-By: Claude Code <noreply@anthropic.com>
53 lines
2.7 KiB
Python
53 lines
2.7 KiB
Python
"""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)
|