Skip to content

Commit 2f35c8e

Browse files
committed
updated token and token parser
1 parent 38c7505 commit 2f35c8e

2 files changed

Lines changed: 226 additions & 56 deletions

File tree

design-tokens/tokens.json

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
{
2-
"colors": {
3-
"primary": {
4-
"500": {
5-
"value": "#3B82F6",
6-
"type": "color"
7-
},
8-
"600": {
9-
"value": "#2563EB",
10-
"type": "color"
2+
"global": {
3+
"colors": {
4+
"primary": {
5+
"500": {
6+
"value": "#3B82F6",
7+
"type": "color"
8+
},
9+
"600": {
10+
"value": "#2563EB",
11+
"type": "color"
12+
}
1113
}
12-
}
13-
},
14-
"spacing": {
15-
"sm": {
16-
"value": "8px",
17-
"type": "dimension"
1814
},
19-
"md": {
20-
"value": "16px",
21-
"type": "dimension"
15+
"spacing": {
16+
"sm": {
17+
"value": "8",
18+
"type": "dimension"
19+
},
20+
"md": {
21+
"value": "16",
22+
"type": "dimension"
23+
}
2224
},
23-
"lg": {
24-
"value": "24px",
25-
"type": "dimension"
25+
"borderRadius": {
26+
"base": {
27+
"value": "4",
28+
"type": "borderRadius"
29+
},
30+
"sm": {
31+
"value": "2",
32+
"type": "borderRadius"
33+
}
2634
}
2735
}
28-
}
36+
}

scripts/figma_parser.py

Lines changed: 196 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
import os
22
import requests
33
import json
4+
from collections import defaultdict
45
from dotenv import load_dotenv
56

7+
from typing import Dict, Any, List, Tuple
8+
69
load_dotenv() # Load environment variables from .env
710

11+
# ================== 配置部分 ==================
812
# 配置参数
913
FIGMA_TOKEN = os.getenv("FIGMA_TOKEN") # 从环境变量读取Token,避免硬编码
1014
FILE_ID = "ZuZWPWcPLSqaX8d1p9uTpX" # 替换为你的Figma文件ID
1115
NODE_IDS = "3-1377" # 替换为你的按钮组件节点ID(多个用逗号分隔)
1216

17+
# 自定义属性名映射表(可扩展)
18+
PROPERTY_ALIAS_MAP = {
19+
"rectangleCornerRadii": "cornerRadius",
20+
"fills": "backgroundColor",
21+
"RECTANGLE_TOP_LEFT_CORNER_RADIUS": "cornerRadius",
22+
"itemSpacing": "spacing"
23+
}
24+
25+
UNMAPPED_PROP_FILE = "unmapped_properties.json"
26+
27+
# ================== 核心工具函数 ==================
1328
def fetch_figma_data():
1429
"""调用Figma API获取节点数据"""
1530
url = f"https://api.figma.com/v1/files/{FILE_ID}/nodes?ids={NODE_IDS}"
@@ -20,45 +35,192 @@ def fetch_figma_data():
2035

2136
return response.json()
2237

23-
def parse_button_data(raw_data):
24-
"""解析Figma数据,提取按钮颜色和尺寸"""
25-
buttons = []
38+
def save_to_json(data, output_path="../design_system/output.json"):
39+
"""保存解析后的数据到JSON文件"""
40+
with open(output_path, "w") as f:
41+
json.dump(data, f, indent=2)
42+
print(f"✅ 数据已保存至 {os.path.abspath(output_path)}")
43+
44+
def rgb_to_hex(rgba: dict) -> str:
45+
"""将Figma的RGBA值转为HEX(忽略alpha)"""
46+
r = int(rgba["r"] * 255)
47+
g = int(rgba["g"] * 255)
48+
b = int(rgba["b"] * 255)
49+
return f"#{r:02x}{g:02x}{b:02x}".upper()
50+
51+
def find_closest_color(hex_value: str, color_tokens: dict) -> str:
52+
"""精确匹配颜色Token,未找到返回HEX值"""
53+
for category, shades in color_tokens.items():
54+
for shade, data in shades.items():
55+
if data["value"].upper() == hex_value.upper():
56+
return f"{category}.{shade}"
57+
return hex_value # 直接返回HEX值
58+
59+
def find_closest_spacing(value: float, spacing_tokens: dict) -> str:
60+
"""统一处理间距和圆角,未匹配时返回{value}px"""
61+
value_px = float(value)
62+
63+
# 遍历所有spacing类别
64+
for category, sizes in spacing_tokens.items():
65+
for size_name, size_data in sizes.items():
66+
if size_name != 'value':
67+
continue
68+
token_value = float(size_data)
69+
if abs(value_px - token_value) <= 0.01:
70+
return category # 找到匹配项,直接返回category
2671

27-
for node_id, node_info in raw_data["nodes"].items():
28-
doc = node_info["document"]
29-
name_parts = doc["name"].split("/") # 假设组件命名为 "Button/Primary"
72+
return f"{int(value_px)}px" # 未匹配时返回24px格式
73+
74+
def resolve_property_name(raw_prop: str) -> str:
75+
"""通过映射表解析属性名"""
76+
return PROPERTY_ALIAS_MAP.get(raw_prop, raw_prop)
77+
78+
def extract_actual_value(node_data: Dict, prop: str) -> Any:
79+
"""
80+
从document中提取实际值,处理特殊结构
81+
返回 (是否成功, 值或错误信息)
82+
"""
83+
try:
84+
# 处理嵌套属性(如rectangleCornerRadii.XXX)
85+
if "." in prop:
86+
parts = prop.split(".")
87+
value = node_data
88+
for p in parts:
89+
value = value[p]
90+
return (True, value)
91+
return (True, node_data[prop])
92+
except KeyError:
93+
return (False, f"Property {prop} not found in document")
94+
except TypeError:
95+
return (False, f"Invalid structure for {prop}")
96+
97+
def match_token(value: Any, token_type: str, tokens: dict) -> str:
98+
"""新版Token匹配函数"""
99+
# try:
100+
# 颜色处理 ================================
101+
if token_type == "color":
102+
# 类型检查1:处理数组结构 (如fills)
103+
if isinstance(value, list):
104+
if not value:
105+
return "transparent" # 空颜色数组默认值
106+
value = value[0].get("color") if isinstance(value[0], dict) else value[0]
30107

31-
# 提取颜色(取第一个填充色)
32-
fills = doc.get("fills", [{}])[0].get("color", {})
33-
color = {
34-
"r": fills.get("r", 0),
35-
"g": fills.get("g", 0),
36-
"b": fills.get("b", 0),
37-
"a": fills.get("a", 1)
38-
}
108+
# 类型检查2:确保是颜色字典
109+
if not isinstance(value, dict) or not all(k in value for k in ("r", "g", "b")):
110+
if isinstance(value, str) and value.startswith("#"):
111+
return find_closest_color(value, tokens.get("colors", {})) # 直接处理HEX字符串
112+
return str(value) # 非标准格式返回原始值
39113

40-
# 提取尺寸
41-
bbox = doc.get("absoluteBoundingBox", {})
42-
width = bbox.get("width", 100) # 默认值100px
43-
height = bbox.get("height", 40) # 默认值40px
114+
# 转换为HEX
115+
hex_val = rgb_to_hex(value)
116+
return find_closest_color(hex_val, tokens.get("colors", {}))
117+
118+
# 间距/圆角处理 ============================
119+
elif token_type in ["spacing", "radius"]:
120+
# 类型检查:转换为数值
121+
num_val = None
122+
if isinstance(value, (int, float)):
123+
num_val = float(value)
124+
elif isinstance(value, str) and "px" in value:
125+
try:
126+
num_val = float(value.replace("px", ""))
127+
except ValueError:
128+
return value # 无法转换时返回原始值
129+
else:
130+
return str(value)
44131

45-
buttons.append({
46-
"component": name_parts[0],
47-
"variant": name_parts[1] if len(name_parts) > 1 else "default",
48-
"color": color,
49-
"size": {"width": width, "height": height}
50-
})
132+
return find_closest_spacing(num_val, tokens.get(token_type, {}))
133+
134+
# 其他类型 ================================
51135

52-
return buttons
136+
# 其他类型直接返回字符串
137+
else:
138+
return str(value)
139+
140+
# except Exception as e:
141+
# print(f"Token匹配错误: {e}")
142+
# return f"error: {str(value)}" # 保留原始值信息
53143

54-
def save_to_json(data, output_path="../design_system/output.json"):
55-
"""保存解析后的数据到JSON文件"""
56-
with open(output_path, "w") as f:
57-
json.dump(data, f, indent=2)
58-
print(f"✅ 数据已保存至 {os.path.abspath(output_path)}")
144+
# ================== 主解析逻辑 ==================
145+
def analyze_node(node_id: str, node_data: Dict, tokens: Dict) -> Tuple[Dict, List]:
146+
"""
147+
解析单个节点数据
148+
返回:(变量映射结果, 未处理的属性列表)
149+
"""
150+
mappings = {}
151+
unmapped = []
152+
153+
bound_vars = node_data.get("document", {}).get("boundVariables", {})
154+
155+
for raw_prop, var_info in bound_vars.items():
156+
# 1. 解析属性名
157+
prop = resolve_property_name(raw_prop)
158+
159+
# 2. 提取实际值
160+
success, value = extract_actual_value(node_data["document"], prop)
161+
if not success:
162+
unmapped.append({
163+
"node_id": node_id,
164+
"raw_property": raw_prop,
165+
"resolved_property": prop,
166+
"error": value
167+
})
168+
continue
169+
170+
# 3. 确定变量类型
171+
token_type = "spacing" if "padding" in prop else \
172+
"color" if "Color" in prop else \
173+
"color" if "color" in prop else \
174+
"radius" if "Radius" in prop else \
175+
"other"
176+
177+
# 4. 匹配或生成Token
178+
token = match_token(value, token_type, tokens)
179+
180+
mappings[raw_prop] = {
181+
"resolved_property": prop,
182+
"value": value,
183+
"token": token,
184+
"token_type": token_type
185+
}
186+
187+
return mappings, unmapped
188+
189+
# ================== 执行示例 ==================
190+
def main():
191+
# 加载现有tokens
192+
with open("/Users/Pan/Projects/Projects/Demo_DesignSystem/demo/scripts/tokens.json") as f:
193+
# with open("tokens.json") as f:
194+
tokens = json.load(f)
195+
196+
# raw_data是API返回的原始数据
197+
with open("/Users/Pan/Projects/Projects/Demo_DesignSystem/demo/scripts/raw.json") as f:
198+
# with open("raw.json") as f:
199+
raw_data = json.load(f)
200+
# raw_data = fetch_figma_data()
201+
202+
# 初始化报告
203+
all_mappings = {}
204+
all_unmapped = []
205+
206+
# 遍历所有节点
207+
for node_id, node_data in raw_data.get("nodes", {}).items():
208+
mappings, unmapped = analyze_node(node_id, node_data, tokens)
209+
all_mappings[node_id] = mappings
210+
all_unmapped.extend(unmapped)
211+
212+
# 保存结果
213+
with open("variable_mappings.json", "w") as f:
214+
json.dump(all_mappings, f, indent=2)
215+
216+
217+
# 记录未处理属性
218+
if all_unmapped:
219+
with open(UNMAPPED_PROP_FILE, "w") as f:
220+
json.dump(all_unmapped, f, indent=2)
221+
print(f"⚠️ 发现 {len(all_unmapped)} 个未处理属性,已保存至 {UNMAPPED_PROP_FILE}")
222+
223+
print("✅ 解析完成!变量映射已保存至 variable_mappings.json")
59224

60225
if __name__ == "__main__":
61-
# 执行流程
62-
raw_data = fetch_figma_data()
63-
buttons = parse_button_data(raw_data)
64-
save_to_json(buttons)
226+
main()

0 commit comments

Comments
 (0)