返回 CodeWhale
export-design-tokens.py
根目录 / scripts / export-design-tokens.py
1 #!/usr/bin/env python3
2 """Export the Codewhale palettes to the other Codewhale clients.
3
4 `crates/palette/src/tokens.rs` is the single source for the product colors.
5 This script parses its `WHALE_*_RGB`, `LIGHT_*_RGB`, `SHORELINE_*_RGB`, and
6 `SHORELINE_LIGHT_*_RGB` consts (aliases included) and writes the same values
7 as CSS custom properties so the web app stops hand-copying hexes. The
8 Shoreline set is the Shoreline redesign's dark + light pair; `--whale-*` and
9 `--light-*` stay until the components that use them migrate.
10
11 Target: <repo>/web/app/tokens.css. This script writes nothing outside this
12 repository.
13
14 Usage:
15 scripts/export-design-tokens.py # write
16 scripts/export-design-tokens.py --check # exit 1 if any target is stale
17 """
18
19 from __future__ import annotations
20
21 import argparse
22 import re
23 import sys
24 from pathlib import Path
25
26 REPO = Path(__file__).resolve().parent.parent
27 TOKENS_RS = REPO / "crates/palette/src/tokens.rs"
28 SOURCE_LABEL = "crates/palette/src/tokens.rs"
29
30 CONST_RE = re.compile(
31 r"^pub const ((?:SHORELINE_LIGHT|SHORELINE|WHALE|LIGHT)_[A-Z0-9_]+)_RGB: \(u8, u8, u8\) = "
32 r"(?:\((\d+), (\d+), (\d+)\)|((?:SHORELINE_LIGHT|SHORELINE|WHALE|LIGHT)_[A-Z0-9_]+)_RGB);",
33 re.MULTILINE,
34 )
35
36
37
38 def parse_tokens(text: str) -> list[tuple[str, tuple[int, int, int] | str]]:
39 """Return [(NAME, (r, g, b) | alias-NAME)] in source order."""
40 tokens: list[tuple[str, tuple[int, int, int] | str]] = []
41 known: set[str] = set()
42 for m in CONST_RE.finditer(text):
43 name = m.group(1)
44 if m.group(5) is not None:
45 target = m.group(5)
46 if target not in known:
47 raise SystemExit(f"{name} aliases unknown token {target}")
48 tokens.append((name, target))
49 else:
50 tokens.append((name, (int(m.group(2)), int(m.group(3)), int(m.group(4)))))
51 known.add(name)
52 if not tokens:
53 raise SystemExit(f"no palette RGB consts found in {TOKENS_RS}")
54 return tokens
55
56
57 def css_name(name: str) -> str:
58 """`WHALE_*` exports as `--whale-*`; the Blue Stage light preset's
59 `LIGHT_*` consts export as `--light-*` so the website's paper surface can
60 reference the same light-mode ink and border values the TUI ships. The
61 Shoreline dark pair exports as `--shoreline-*` and its light pair as
62 `--shoreline-light-*`, matching the theme the TUI and GPUI clients open
63 on."""
64 if name.startswith("SHORELINE_LIGHT_"):
65 return "--shoreline-light-" + name.removeprefix("SHORELINE_LIGHT_").lower().replace(
66 "_", "-"
67 )
68 if name.startswith("SHORELINE_"):
69 return "--shoreline-" + name.removeprefix("SHORELINE_").lower().replace("_", "-")
70 if name.startswith("LIGHT_"):
71 return "--light-" + name.removeprefix("LIGHT_").lower().replace("_", "-")
72 return "--whale-" + name.removeprefix("WHALE_").lower().replace("_", "-")
73
74
75 def render_css(tokens) -> str:
76 lines = [
77 f"/* generated from {SOURCE_LABEL} — do not edit */",
78 "/* regenerate: scripts/export-design-tokens.py (in the codewhale repo) */",
79 ":root {",
80 ]
81 for name, value in tokens:
82 prop = css_name(name)
83 if isinstance(value, str):
84 ref = css_name(value)
85 lines.append(f" {prop}: var({ref});")
86 lines.append(f" {prop}-rgb: var({ref}-rgb);")
87 else:
88 r, g, b = value
89 lines.append(f" {prop}: #{r:02x}{g:02x}{b:02x};")
90 lines.append(f" {prop}-rgb: {r} {g} {b};")
91 lines.append("}")
92 return "\n".join(lines) + "\n"
93
94
95 def main() -> int:
96 ap = argparse.ArgumentParser(description=__doc__)
97 ap.add_argument("--check", action="store_true", help="verify instead of write")
98 args = ap.parse_args()
99
100 tokens = parse_tokens(TOKENS_RS.read_text(encoding="utf-8"))
101 css = render_css(tokens)
102
103 targets: list[tuple[Path, str]] = [(REPO / "web/app/tokens.css", css)]
104
105 stale = []
106 for path, content in targets:
107 current = path.read_text(encoding="utf-8") if path.exists() else None
108 if current == content:
109 continue
110 if args.check:
111 stale.append(path)
112 else:
113 path.parent.mkdir(parents=True, exist_ok=True)
114 path.write_text(content, encoding="utf-8")
115 print(f"wrote {path}")
116
117 if stale:
118 for path in stale:
119 print(f"stale: {path}", file=sys.stderr)
120 print(
121 "run scripts/export-design-tokens.py to regenerate from "
122 f"{SOURCE_LABEL}",
123 file=sys.stderr,
124 )
125 return 1
126 if args.check:
127 print(f"design tokens up to date ({len(targets)} file(s), {len(tokens)} tokens)")
128 return 0
129
130
131 if __name__ == "__main__":
132 sys.exit(main())
133
133 lines PYTHON