1+ #!/usr/bin/env python3
2+ """
3+ fix-mdx-styles.py – Perfect JSX style conversion
4+ Fixes:
5+ • style="width: 100%" → style={{ width: "100%" }}
6+ • style="margin: 2rem" → style={{ margin: "2rem" }}
7+ • Handles percentages, px, rem, colors, var(), calc(), etc.
8+ • No extra spaces inside {{ }}
9+ • Works with symlinks and large repos (Layer5 tested)
10+ """
11+
12+ import re
13+ import sys
14+ from pathlib import Path
15+
16+ DRY_RUN = "--dry" in sys .argv
17+ VERBOSE = "--verbose" in sys .argv or "-v" in sys .argv
18+
19+
20+ def css_value_needs_quotes (value : str ) -> bool :
21+ """
22+ Return True if the CSS value must be wrapped in quotes in JSX.
23+ Examples that DO need quotes: 100%, 2rem, center, relative, "some text"
24+ Examples that DO NOT: 42, 3.14, #fff, var(--color), calc(100% - 20px)
25+ """
26+ value = value .strip ()
27+
28+ # These are safe without quotes
29+ if re .match (r"^(\d+\.?\d*|\.\d+)(px|rem|em|%|vh|vw|deg|grad|rad|turn|s|ms)?$" , value , re .I ):
30+ return True # 100%, 16px, 2.5rem → needs quotes!
31+ if re .match (r"^(#|rgb|hsl|var\(|calc\(|url\(|inherit|initial|unset|transparent|none|auto)" , value , re .I ):
32+ return False
33+ if value .isdigit ():
34+ return False
35+
36+ # pure numbers like flex: 1
37+ return True # everything else: center, relative, "flex", etc.
38+
39+
40+ def css_to_jsx (css : str ) -> str :
41+ css = css .strip ()
42+ if not css :
43+ return "{}"
44+ if "{{" in css or "}}" in css :
45+ return css
46+
47+ rules = [r .strip () for r in css .split (";" ) if r .strip ()]
48+ entries = []
49+
50+ for rule in rules :
51+ if ":" not in rule :
52+ continue
53+ key , val = rule .split (":" , 1 )
54+ key = key .strip ()
55+ val = val .strip ().rstrip (";" ).strip ()
56+ if not key :
57+ continue
58+
59+ # kebab → camelCase
60+ key = re .sub (r"-([a-zA-Z])" , lambda m : m .group (1 ).upper (), key )
61+
62+ # Quote only when necessary — but % values ALWAYS need quotes
63+ if css_value_needs_quotes (val ):
64+ val = f'"{ val .replace ('"' , '\\ "' )} "'
65+
66+ entries .append (f"{ key } : { val } " )
67+
68+ return "{ " + ", " .join (entries ) + " }" if entries else "{}"
69+
70+
71+ def fix_file (filepath : Path ) -> tuple [bool , int ]:
72+ try :
73+ text = filepath .read_text (encoding = "utf-8" )
74+ except Exception as e :
75+ print (f"Skipping { filepath } : { e } " )
76+ return False , 0
77+
78+ def repl (match ):
79+ css_content = match .group (2 )
80+ jsx_obj = css_to_jsx (css_content )
81+ return f"style={{{ jsx_obj } }}"
82+
83+ new_text , count = re .subn (
84+ r'style\s*=\s*(["\'])(.+?)\1' ,
85+ repl ,
86+ text ,
87+ flags = re .DOTALL
88+ )
89+
90+ if count > 0 and new_text != text :
91+ if not DRY_RUN :
92+ filepath .write_text (new_text , encoding = "utf-8" )
93+ return True , count
94+ return False , 0
95+
96+
97+ def safe_path (p : Path ) -> str :
98+ try :
99+ return p .relative_to (Path .cwd ()).as_posix ()
100+ except ValueError :
101+ return p .as_posix ()
102+
103+
104+ def main ():
105+ root = Path ("." )
106+ mdx_files = list (root .rglob ("*.mdx" ))
107+
108+ print (f"Scanning { len (mdx_files )} .mdx files...\n " )
109+
110+ changed_files = 0
111+ total_fixes = 0
112+
113+ for file in sorted (mdx_files ):
114+ changed , fixes = fix_file (file )
115+ if changed :
116+ changed_files += 1
117+ total_fixes += fixes
118+ status = "(dry run)" if DRY_RUN else "FIXED"
119+ print (f"{ status } → { safe_path (file )} ({ fixes } style(s))" )
120+
121+ print ("\n " + "=" * 60 )
122+ print ("Conversion Complete!" )
123+ print (f" Files scanned : { len (mdx_files )} " )
124+ print (f" Files changed : { changed_files } " )
125+ print (f" Style fixes : { total_fixes } " )
126+
127+ if DRY_RUN :
128+ print ("\n DRY RUN — no files were modified." )
129+ print (" Run without --dry to apply changes." )
130+ else :
131+ print ("\n All style attributes now use perfect JSX syntax!" )
132+
133+
134+ if __name__ == "__main__" :
135+ main ()
0 commit comments