Document wemini credentials and native weapp workflow in README; add jscode2session auth endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
259 lines
7.3 KiB
Python
259 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Sanitize frontend globals.css into WeChat WXSS-safe nomadro.css.
|
|
|
|
WXSS rejects: *, color-mix(), @supports, @layer, :has/:is/:where, html/body
|
|
tag chains that confuse the compiler, etc. Keep H5 class names & tokens.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SRC = ROOT / "frontend" / "src" / "app" / "globals.css"
|
|
OUT = ROOT / "wemini" / "src" / "styles" / "nomadro.css"
|
|
|
|
|
|
def unwrap_blocks(text: str, opener_re: str) -> str:
|
|
"""Remove @foo { ... } blocks (balanced braces), keep inner only for @layer."""
|
|
keep_inner = "@layer" in opener_re
|
|
while True:
|
|
m = re.search(opener_re, text)
|
|
if not m:
|
|
break
|
|
start = m.start()
|
|
i = m.end()
|
|
depth = 1
|
|
while i < len(text) and depth:
|
|
if text[i] == "{":
|
|
depth += 1
|
|
elif text[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
inner = text[m.end() : i - 1]
|
|
text = text[:start] + (inner if keep_inner else "") + text[i:]
|
|
return text
|
|
|
|
|
|
def drop_rules_matching(text: str, selector_test) -> str:
|
|
"""Drop full CSS rules whose selector string fails selector_test(sel)->bool keep."""
|
|
out = []
|
|
i = 0
|
|
n = len(text)
|
|
while i < n:
|
|
if text[i] == "/" and i + 1 < n and text[i + 1] == "*":
|
|
j = text.find("*/", i + 2)
|
|
if j < 0:
|
|
break
|
|
out.append(text[i : j + 2])
|
|
i = j + 2
|
|
continue
|
|
if text[i] == "@" and not text[i:].startswith("@keyframes") and not text[i:].startswith("@media") and not text[i:].startswith("@font-face"):
|
|
# skip unknown at-rules with blocks
|
|
m = re.match(r"@[^{;]+[;{]", text[i:])
|
|
if m and m.group(0).endswith("{"):
|
|
depth = 1
|
|
j = i + m.end()
|
|
while j < n and depth:
|
|
if text[j] == "{":
|
|
depth += 1
|
|
elif text[j] == "}":
|
|
depth -= 1
|
|
j += 1
|
|
i = j
|
|
continue
|
|
elif m and m.group(0).endswith(";"):
|
|
i = i + m.end()
|
|
continue
|
|
|
|
# find next { for a rule
|
|
brace = text.find("{", i)
|
|
if brace < 0:
|
|
out.append(text[i:])
|
|
break
|
|
# if @keyframes / @media — copy whole balanced block
|
|
chunk_head = text[i:brace].lstrip()
|
|
if chunk_head.startswith("@keyframes") or chunk_head.startswith("@media") or chunk_head.startswith("@font-face"):
|
|
depth = 1
|
|
j = brace + 1
|
|
while j < n and depth:
|
|
if text[j] == "{":
|
|
depth += 1
|
|
elif text[j] == "}":
|
|
depth -= 1
|
|
j += 1
|
|
block = text[i:j]
|
|
# still sanitize color-mix inside
|
|
out.append(block)
|
|
i = j
|
|
continue
|
|
|
|
sel = text[i:brace]
|
|
depth = 1
|
|
j = brace + 1
|
|
while j < n and depth:
|
|
if text[j] == "{":
|
|
depth += 1
|
|
elif text[j] == "}":
|
|
depth -= 1
|
|
j += 1
|
|
body = text[brace:j]
|
|
if selector_test(sel):
|
|
out.append(sel + body)
|
|
i = j
|
|
return "".join(out)
|
|
|
|
|
|
def selector_ok(sel: str) -> bool:
|
|
s = sel.strip()
|
|
if not s:
|
|
return False
|
|
# comments-only
|
|
if s.startswith("/*") and "*/" in s and s.rstrip().endswith("*/"):
|
|
return True
|
|
# wildcard
|
|
if re.search(r"(^|[\s,])\*", s):
|
|
return False
|
|
if ":has(" in s or ":is(" in s or ":where(" in s:
|
|
return False
|
|
# raw HTML tags as primary selectors (weapp has no div/span/html/body)
|
|
bad_tags = (
|
|
r"\bhtml\b",
|
|
r"\bbody\b",
|
|
r"\bdiv\b",
|
|
r"\bspan\b",
|
|
r"\ba\b",
|
|
r"\bbutton\b",
|
|
r"\binput\b",
|
|
r"\bselect\b",
|
|
r"\btextarea\b",
|
|
r"\bimg\b",
|
|
r"\bsvg\b",
|
|
r"\bpath\b",
|
|
r"\bcircle\b",
|
|
r"\bellipse\b",
|
|
r"\bul\b",
|
|
r"\bol\b",
|
|
r"\bli\b",
|
|
r"\bh[1-6]\b",
|
|
r"\bp\b",
|
|
r"\bnav\b",
|
|
r"\bfooter\b",
|
|
r"\bheader\b",
|
|
r"\bsection\b",
|
|
r"\barticle\b",
|
|
r"\bmain\b",
|
|
r"\baside\b",
|
|
r"\blabel\b",
|
|
r"\btable\b",
|
|
r"\btr\b",
|
|
r"\btd\b",
|
|
r"\bth\b",
|
|
)
|
|
# Allow .class h2 etc? Those still break WXSS for unknown tags — drop rules with tag segments
|
|
# Keep if selector is only classes/ids/pseudo on classes
|
|
parts = re.split(r"\s*,\s*", s)
|
|
for part in parts:
|
|
part = part.strip()
|
|
if not part or part.startswith("/*"):
|
|
continue
|
|
# strip :pseudo and ::pseudo
|
|
bare = re.sub(r"::?[a-zA-Z0-9_-]+(\([^)]*\))?", "", part)
|
|
bare = re.sub(r"\[[^\]]*\]", "", bare)
|
|
# if any raw tag token remains
|
|
tokens = re.findall(r"[.#]?[A-Za-z_][\w-]*", bare)
|
|
for t in tokens:
|
|
if t.startswith(".") or t.startswith("#"):
|
|
continue
|
|
if t.lower() in {
|
|
"html", "body", "div", "span", "a", "button", "input", "select", "textarea",
|
|
"img", "svg", "path", "circle", "ellipse", "ul", "ol", "li",
|
|
"h1", "h2", "h3", "h4", "h5", "h6", "p", "nav", "footer", "header",
|
|
"section", "article", "main", "aside", "label", "table", "tr", "td", "th",
|
|
"from", "to", "small", "strong", "em", "code", "pre", "blockquote",
|
|
}:
|
|
return False
|
|
return True
|
|
|
|
|
|
def replace_color_mix(text: str) -> str:
|
|
# color-mix(in srgb, var(--x) N%, transparent) → approx rgba via opacity on solid fallback
|
|
def repl(m: re.Match) -> str:
|
|
inner = m.group(1)
|
|
# var(--token) pct%, transparent
|
|
mm = re.search(
|
|
r"var\((--[\w-]+)\)\s+(\d+)%\s*,\s*transparent",
|
|
inner,
|
|
)
|
|
if mm:
|
|
var, pct = mm.group(1), int(mm.group(2))
|
|
# keep a readable fallback; opacity via 8-digit hex not always ok — use the var at lower intent
|
|
return f"rgba(255,255,255,{pct/100 * 0.35:.2f}) /* was color-mix {var} {pct}% */"
|
|
mm2 = re.search(r"transparent\s*,\s*var\((--[\w-]+)\)\s+(\d+)%", inner)
|
|
if mm2:
|
|
return f"var({mm2.group(1)})"
|
|
return "transparent"
|
|
|
|
return re.sub(r"color-mix\(([^)]*)\)", repl, text)
|
|
|
|
|
|
def main() -> int:
|
|
src = SRC.read_text(encoding="utf-8")
|
|
src = re.sub(
|
|
r"html:has\(\[data-ebook-root\]\)[^{]*\{(?:[^{}]|\{[^{}]*\})*\}",
|
|
"",
|
|
src,
|
|
)
|
|
src = unwrap_blocks(src, r"@layer\s+[\w-]+\s*\{")
|
|
src = unwrap_blocks(src, r"@supports\b[^{]*\{")
|
|
# second pass: any leftover @supports
|
|
while True:
|
|
m = re.search(r"@supports\b[^{]*\{", src)
|
|
if not m:
|
|
break
|
|
i = m.end()
|
|
depth = 1
|
|
while i < len(src) and depth:
|
|
if src[i] == "{":
|
|
depth += 1
|
|
elif src[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
src = src[: m.start()] + src[i:]
|
|
src = src.replace("var(--font-outfit), ", "").replace(", var(--font-outfit)", "")
|
|
src = replace_color_mix(src)
|
|
src = re.sub(r"\binset:\s*0\s*;", "top:0;right:0;bottom:0;left:0;", src)
|
|
src = re.sub(r"\binset:\s*([^;]+);", r"top:\1;right:\1;bottom:\1;left:\1;", src)
|
|
# drop bad rules
|
|
src = drop_rules_matching(src, selector_ok)
|
|
# page root from :root + body-ish defaults
|
|
page_patch = """
|
|
page {
|
|
background: var(--bg-primary, #0c0b12);
|
|
color: var(--text-primary, #f4f0ea);
|
|
font-size: 28rpx;
|
|
line-height: 1.6;
|
|
box-sizing: border-box;
|
|
}
|
|
"""
|
|
header = "/* WXSS-safe mirror of frontend globals.css (H5 class names kept) */\n"
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUT.write_text(header + src + page_patch, encoding="utf-8")
|
|
# quick scan
|
|
bad = []
|
|
for pat, name in [
|
|
(r"(^|[\s,])\*\s*[,{]", "*"),
|
|
(r"color-mix\(", "color-mix"),
|
|
(r"@supports", "@supports"),
|
|
(r"@layer", "@layer"),
|
|
(r":has\(", ":has"),
|
|
]:
|
|
if re.search(pat, OUT.read_text(encoding="utf-8"), re.M):
|
|
bad.append(name)
|
|
print(f"wrote {OUT} ({OUT.stat().st_size} bytes) leftover={bad or 'none'}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|