100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
"""One-off converter: the CNX/Mooban view CSS -> isokit theme TOML.
|
|
|
|
python tools/css2theme.py C:/dev/mooban/tools/estate-view/iso-mooban.css themes/default.toml
|
|
|
|
Palette blocks (`.iso-wrap { --iso-x: ... }` and the light block) become
|
|
[palette.dark] / [palette.light]; one-class rules that only set `--b` become
|
|
[materials]; every other plain rule becomes [rules]. Structural rules (stage,
|
|
toolbar, tooltip, faces, tags, selection, animation, full screen) live in
|
|
viewer/isokit-view.css and are skipped here.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
STRUCTURAL = re.compile(
|
|
r"iso-(wrap|stage|svg$|bar|seg|compass|tip|note|fs|fullbtn|tag\b|tag |signtag|warntag|hide|ghost|hov|sel|dim|"
|
|
r"dragging|off|still|fan\b|puff|smoke|glint|disc\b|walk|crane\b|tin-|tout-|led[rgo]?\b|park\b|drive|a\b)"
|
|
)
|
|
FACE = re.compile(r"\.iso-(t|l|c|r)\b(?![-\w])")
|
|
|
|
|
|
def blocks(css):
|
|
css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
|
i, n = 0, len(css)
|
|
while i < n:
|
|
j = css.find("{", i)
|
|
if j < 0:
|
|
break
|
|
sel = css[i:j].strip()
|
|
if sel.startswith("@"):
|
|
depth, k = 1, j + 1
|
|
while depth and k < n:
|
|
depth += {"{": 1, "}": -1}.get(css[k], 0)
|
|
k += 1
|
|
i = k
|
|
continue
|
|
k = css.find("}", j)
|
|
yield sel, css[j + 1:k].strip()
|
|
i = k + 1
|
|
|
|
|
|
def decls(body):
|
|
out = []
|
|
for d in body.split(";"):
|
|
d = d.strip()
|
|
if d:
|
|
k, v = d.split(":", 1)
|
|
out.append((k.strip(), v.strip()))
|
|
return out
|
|
|
|
|
|
def q(s):
|
|
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
|
|
|
|
def main(src, dst):
|
|
css = open(src, encoding="utf-8").read()
|
|
dark, light, mats, rules = {}, {}, {}, {}
|
|
for sel, body in blocks(css):
|
|
ds = decls(body)
|
|
if sel in (".iso-wrap", "html[data-theme=light] .iso-wrap"):
|
|
pal = dark if sel == ".iso-wrap" else light
|
|
for k, v in ds:
|
|
if k.startswith("--iso-"):
|
|
pal[k[6:]] = v
|
|
continue
|
|
m = re.fullmatch(r"\.iso-([\w-]+)", sel)
|
|
if m and len(ds) == 1 and ds[0][0] == "--b":
|
|
v = ds[0][1]
|
|
vm = re.fullmatch(r"var\(--iso-([\w-]+)\)", v)
|
|
mats[m.group(1)] = vm.group(1) if vm else v
|
|
continue
|
|
parts = [p.strip() for p in sel.split(",")]
|
|
if any(STRUCTURAL.search(p) for p in parts) or all(FACE.search(p) and "iso-svg .iso-" in p and len(p.split()) == 2 for p in parts):
|
|
continue
|
|
rules[sel] = "; ".join(f"{k}: {v}" for k, v in ds)
|
|
with open(dst, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write("# isokit default theme, converted from the CNX Vinyl / CNX Scuba / Mooban views.\n")
|
|
f.write("# palette.*: CSS variables --iso-<name> (dark is the default, light applies under\n")
|
|
f.write("# html[data-theme=light]); materials: class iso-<name> sets the face colour --b to a\n")
|
|
f.write("# palette entry or a literal colour; rules: any other CSS, selector = declarations.\n")
|
|
f.write("# A project theme file uses the same tables and overrides entries by key.\n\n")
|
|
f.write('name = "default"\n\n')
|
|
for name, pal in (("dark", dark), ("light", light)):
|
|
f.write(f"[palette.{name}]\n")
|
|
for k, v in pal.items():
|
|
f.write(f"{q(k) if not re.fullmatch(r'[A-Za-z0-9_-]+', k) else k} = {q(v)}\n")
|
|
f.write("\n")
|
|
f.write("[materials]\n")
|
|
for k, v in mats.items():
|
|
f.write(f"{k} = {q(v)}\n")
|
|
f.write("\n[rules]\n")
|
|
for k, v in rules.items():
|
|
f.write(f"{q(k)} = {q(v)}\n")
|
|
print(f"palette dark {len(dark)}, light {len(light)}, materials {len(mats)}, rules {len(rules)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1], sys.argv[2])
|