#!/usr/bin/env python3
"""
Match Figma cloud Drafts files (from figma-files.json) back to W{N} entries in
manifest.json by fuzzy name match, then update web/data.json with figma_key+url.
"""
import json, re, sys, difflib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def normalize(s):
s = s.lower()
s = re.sub(r'[^a-z0-9]+', ' ', s).strip()
return s
def best_match(target, candidates):
"""Return (score, candidate) best match from candidates list of dicts with .name"""
nt = normalize(target)
best = (0, None)
for c in candidates:
nc = normalize(c['name'])
# exact / prefix / contains / sequence
if nt == nc: s = 1.0
elif nt in nc or nc in nt: s = 0.85
else: s = difflib.SequenceMatcher(None, nt, nc).ratio()
if s > best[0]:
best = (s, c)
return best
def main():
manifest = json.loads((ROOT/'manifest.json').read_text())
figma_files_path = ROOT/'figma-files.json'
if not figma_files_path.exists():
print(f"missing {figma_files_path}", file=sys.stderr)
sys.exit(1)
figma_files = json.loads(figma_files_path.read_text()) # list of {key, name, ...}
# Match each template (fig preferred, else sketch) against cloud files
matches = []
used_keys = set()
for t in manifest['templates']:
source_files = t['fig'] if t['fig'] else t['sketch']
if not source_files: continue
kind = 'fig' if t['fig'] else 'sketch'
targets = [Path(f).stem for f in source_files]
if t.get('archive'):
targets.append(Path(t['archive']).stem)
targets.append(t['name'])
best_overall = (0, None)
for tgt in targets:
score, cand = best_match(tgt, [f for f in figma_files if f['key'] not in used_keys])
if score > best_overall[0]:
best_overall = (score, cand)
if score > 0.95:
break
score, cand = best_overall
stem = Path(source_files[0]).stem
if cand and score >= 0.6:
used_keys.add(cand['key'])
matches.append({
'W': t['id'], 'name': t['name'], 'kind': kind, 'source_stem': stem,
'matched': cand['name'], 'key': cand['key'], 'score': round(score, 3)
})
else:
matches.append({
'W': t['id'], 'name': t['name'], 'kind': kind, 'source_stem': stem,
'matched': None, 'best_score': round(best_overall[0], 3) if cand else 0,
'best_candidate': cand['name'] if cand else None
})
# Update web/data.json
data_path = ROOT/'web'/'data.json'
data = json.loads(data_path.read_text())
by_W = {m['W']: m for m in matches if m.get('key')}
for t in data['templates']:
m = by_W.get(t['id'])
if m:
t['figma_key'] = m['key']
t['figma_url'] = f"https://www.figma.com/file/{m['key']}"
else:
t['figma_key'] = None
t['figma_url'] = None
# update banner with imported count
imported = sum(1 for t in data['templates'] if t['figma_key'])
fig_cnt = sum(1 for t in manifest['templates'] if t['fig'])
sketch_only_cnt = sum(1 for t in manifest['templates'] if not t['fig'] and t['sketch'])
data['imported_summary'] = (
f"✅ {imported}/56 套已云端就位在 "
f"Figma Drafts"
f"({fig_cnt} 个 .fig 原生 + {sketch_only_cnt} 个 .sketch 经 Figma 转换)。"
f"点卡片 → modal → iframe 实时投射。"
)
data_path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
# write match report
report = ROOT/'figma-match-report.json'
report.write_text(json.dumps(matches, ensure_ascii=False, indent=2))
total_matchable = sum(1 for t in manifest['templates'] if t['fig'] or t['sketch'])
print(f"Matched {imported}/{total_matchable} templates ({fig_cnt} fig + {sketch_only_cnt} sketch-only)")
print(f"Report: {report.relative_to(ROOT)}")
unmatched = [m for m in matches if not m.get('key')]
if unmatched:
print(f"\n⚠️ {len(unmatched)} unmatched:")
for m in unmatched:
print(f" {m['W']:4s} {m['name'][:50]:50s} best='{m.get('best_candidate','')[:40]}' score={m.get('best_score',0)}")
if __name__ == '__main__':
main()