11import os
22import requests
33import json
4+ from collections import defaultdict
45from dotenv import load_dotenv
56
7+ from typing import Dict , Any , List , Tuple
8+
69load_dotenv () # Load environment variables from .env
710
11+ # ================== 配置部分 ==================
812# 配置参数
913FIGMA_TOKEN = os .getenv ("FIGMA_TOKEN" ) # 从环境变量读取Token,避免硬编码
1014FILE_ID = "ZuZWPWcPLSqaX8d1p9uTpX" # 替换为你的Figma文件ID
1115NODE_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+ # ================== 核心工具函数 ==================
1328def 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
60225if __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