"""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- (dark is the default, light applies under\n") f.write("# html[data-theme=light]); materials: class iso- 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])