themed_styler/
lib.rs

1use indexmap::{IndexMap, IndexSet};
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6use wasm_bindgen::prelude::*;
7mod default_state;
8use default_state::bundled_state;
9
10pub type CssProps = IndexMap<String, serde_json::Value>;
11pub type SelectorStyles = IndexMap<String, CssProps>; // selector -> props
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct ThemeEntry {
15    #[serde(default)]
16    pub inherits: Option<String>,
17    #[serde(default)]
18    pub selectors: SelectorStyles,
19    #[serde(default)]
20    pub variables: IndexMap<String, String>,
21    #[serde(default)]
22    pub breakpoints: IndexMap<String, String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct State {
27    // New format: each theme has selectors, variables, breakpoints, and optional inherits
28    pub themes: IndexMap<String, ThemeEntry>,
29    pub default_theme: String,
30    pub current_theme: String,
31    // Legacy fields (kept for backward-compat JSON). Not used if themes[] carry variables/bps.
32    #[serde(default)]
33    pub theme_variables: IndexMap<String, IndexMap<String, String>>, // deprecated
34    #[serde(default)]
35    pub variables: IndexMap<String, String>, // deprecated global
36    #[serde(default)]
37    pub breakpoints: IndexMap<String, String>, // deprecated global
38    #[serde(default)]
39    pub used_selectors: IndexSet<String>,
40    #[serde(default)]
41    pub used_classes: IndexSet<String>,
42}
43
44#[derive(thiserror::Error, Debug)]
45pub enum Error {
46    #[error("theme not found: {0}")]
47    ThemeNotFound(String),
48}
49
50impl State {
51    pub fn new_default() -> Self {
52        // Prefer embedded Rust bundled defaults
53        return bundled_state();
54    }
55
56    /// Public helper to access the embedded default state.
57    pub fn default_state() -> Self {
58        bundled_state()
59    }
60
61    pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
62        let name = theme.into();
63        if !self.themes.contains_key(&name) {
64            return Err(Error::ThemeNotFound(name));
65        }
66        self.current_theme = name;
67        Ok(())
68    }
69
70    pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
71        let name = name.into();
72        let entry = self.themes.entry(name).or_default();
73        for (sel, props) in styles.into_iter() {
74            let e = entry.selectors.entry(sel).or_default();
75            merge_props(e, &props);
76        }
77    }
78
79    pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
80        // Back-compat: set on current theme entry
81        let cur = self.current_theme.clone();
82        let entry = self.themes.entry(cur).or_default();
83        entry.variables = vars;
84    }
85
86    pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
87        let cur = self.current_theme.clone();
88        let entry = self.themes.entry(cur).or_default();
89        entry.breakpoints = map;
90    }
91
92    pub fn set_default_theme(&mut self, name: impl Into<String>) {
93        self.default_theme = name.into();
94    }
95
96    pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, selectors: I) {
97        for s in selectors {
98            self.used_selectors.insert(s);
99        }
100    }
101
102    pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
103        for c in classes {
104            self.used_classes.insert(c);
105        }
106    }
107
108
109    pub fn clear_usage(&mut self) {
110        self.used_selectors.clear();
111        self.used_classes.clear();
112    }
113
114    pub fn to_json(&self) -> serde_json::Value {
115        json!({
116            "themes": self.themes,
117            "default_theme": self.default_theme,
118            "current_theme": self.current_theme,
119            // legacy fields are still serialized for back-compat but may be empty
120            "theme_variables": self.theme_variables,
121            "variables": self.variables,
122            "breakpoints": self.breakpoints,
123            "used_selectors": self.used_selectors,
124            "used_classes": self.used_classes,
125        })
126    }
127
128    pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
129        let state: State = serde_json::from_value(value)?;
130        Ok(state)
131    }
132
133    pub fn css_for_web(&self) -> String {
134        // Compute CSS for used selectors + used classes resolved from the effective theme (with inheritance)
135        let (eff, vars) = self.effective_theme_all();
136        let bps = self.effective_breakpoints();
137        let mut rules: Vec<(String, CssProps)> = Vec::new();
138
139        for sel in &self.used_selectors {
140            if let Some(props) = eff.get(sel) {
141                rules.push((sel.clone(), props.clone()));
142            }
143        }
144
145        for class in &self.used_classes {
146            let (bp_key, hover, base) = parse_prefixed_class(class);
147            let selector = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
148
149            // 1) Exact selector in effective theme (e.g. ".x:hover")
150            if let Some(props) = eff.get(&selector) {
151                let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
152                rules.push((final_sel, props.clone()));
153                continue;
154            }
155            // 2) Dynamic generation for the base class (ignoring hover/breakpoint for props)
156            if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
157                let sel = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
158                let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
159                rules.push((final_sel, dynamic_props));
160                continue;
161            }
162            // 3) Fallback: class key itself in theme (rare)
163            if let Some(props) = eff.get(&base) {
164                let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
165                rules.push((final_sel, props.clone()));
166            }
167        }
168
169        post_process_css(&rules, &vars)
170    }
171
172    pub fn rn_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
173        let (eff, vars) = self.effective_theme_all();
174        let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
175        if let Some(props) = eff.get(selector) {
176            merge_rn_props(&mut out, props, &vars);
177        }
178        for class in classes {
179            let (_bp, _hover, base) = parse_prefixed_class(class);
180            // Prefer base selector match from theme
181            let sel = class_to_selector(&base);
182            if let Some(props) = eff.get(&sel) {
183                merge_rn_props(&mut out, props, &vars);
184                continue;
185            }
186            // Dynamic mapping for base class
187            if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
188                merge_rn_props(&mut out, &dynamic_props, &vars);
189                continue;
190            }
191            if let Some(props) = eff.get(&base) {
192                merge_rn_props(&mut out, props, &vars);
193            }
194        }
195        out
196    }
197
198    // Previously supported loading YAML at runtime; now defaults are embedded.
199
200    // Build the inheritance chain from current theme upward via `inherits` and default fallback
201    fn theme_chain(&self) -> Vec<String> {
202        let mut chain = Vec::new();
203        // Resolve base names
204        let default_name = if self.themes.contains_key(&self.default_theme) {
205            self.default_theme.clone()
206        } else if let Some((k, _)) = self.themes.first() { k.clone() } else { return chain };
207        let mut current_name = if self.themes.contains_key(&self.current_theme) {
208            self.current_theme.clone()
209        } else { default_name.clone() };
210        // push child first
211        let mut seen: IndexSet<String> = IndexSet::new();
212        while !seen.contains(&current_name) {
213            seen.insert(current_name.clone());
214            chain.push(current_name.clone());
215            // next parent via inherits, else stop
216            let inherits = self.themes.get(&current_name).and_then(|t| t.inherits.clone());
217            if let Some(p) = inherits {
218                current_name = p;
219            } else {
220                break;
221            }
222        }
223        if !chain.iter().any(|n| n == &default_name) {
224            chain.push(default_name);
225        }
226        chain
227    }
228
229    // Compute effective selectors + variables + breakpoints with inheritance. Parent overrides child.
230    fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
231        let mut selectors: SelectorStyles = SelectorStyles::new();
232        let mut vars: IndexMap<String, String> = IndexMap::new();
233        // Start with deprecated globals as the lowest base
234        for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
235        // Merge child first then parents, with later merges overriding
236        let chain = self.theme_chain();
237        for name in chain.into_iter() {
238            if let Some(entry) = self.themes.get(&name) {
239                // merge selectors: later (parent) overrides child
240                for (sel, props) in entry.selectors.iter() {
241                    let e = selectors.entry(sel.clone()).or_default();
242                    merge_props(e, props);
243                }
244                // merge variables
245                for (k, v) in entry.variables.iter() {
246                    vars.insert(k.clone(), v.clone());
247                }
248            }
249        }
250        (selectors, vars)
251    }
252
253    // Effective breakpoints with inheritance; parent overrides child.
254    pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
255        let mut bps: IndexMap<String, String> = IndexMap::new();
256        // Start with deprecated globals
257        for (k, v) in self.breakpoints.iter() { bps.insert(k.clone(), v.clone()); }
258        let chain = self.theme_chain();
259        for name in chain.into_iter() {
260            if let Some(entry) = self.themes.get(&name) {
261                for (k, v) in entry.breakpoints.iter() {
262                    bps.insert(k.clone(), v.clone());
263                }
264            }
265        }
266        bps
267    }
268}
269
270// wasm-bindgen exports
271#[wasm_bindgen]
272pub fn render_css_for_web(state_json: &str) -> String {
273    match serde_json::from_str::<State>(state_json) {
274        Ok(s) => s.css_for_web(),
275        Err(_) => "".into(),
276    }
277}
278
279#[wasm_bindgen]
280pub fn get_rn_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
281    let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
282    match serde_json::from_str::<State>(state_json) {
283        Ok(s) => serde_json::to_string(&s.rn_styles_for(selector, &classes)).unwrap_or_else(|_| "{}".into()),
284        Err(_) => "{}".into(),
285    }
286}
287
288// Expose crate version to JS via wasm-bindgen
289#[wasm_bindgen]
290pub fn get_version() -> String {
291    // CARGO_PKG_VERSION is provided at compile time
292    env!("CARGO_PKG_VERSION").to_string()
293}
294
295// Plain Rust accessor for crate version used by Android JNI glue
296pub fn version() -> &'static str {
297    env!("CARGO_PKG_VERSION")
298}
299
300/// Return the embedded default state as a JSON string.
301#[wasm_bindgen]
302pub fn get_default_state_json() -> String {
303    let st = bundled_state();
304    match serde_json::to_string(&st.to_json()) {
305        Ok(s) => s,
306        Err(_) => "{}".to_string(),
307    }
308}
309
310fn merge_props(into: &mut CssProps, from: &CssProps) {
311    for (k, v) in from.iter() {
312        into.insert(k.clone(), v.clone());
313    }
314}
315
316// merge_indexmap removed — unused
317
318fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
319    let mut buf = String::new();
320    for (k, v) in props.iter() {
321        buf.push_str(k);
322        buf.push(':');
323        let val = if v.is_string() {
324            let s = v.as_str().unwrap();
325            resolve_vars(s, vars)
326        } else {
327            v.to_string()
328        };
329        buf.push_str(&val);
330        if !val.ends_with(';') {
331            buf.push(';');
332        }
333    }
334    buf
335}
336
337// Support var(--name) and var(name)
338static RE_VAR: Lazy<Regex> = Lazy::new(|| Regex::new(r"var\(\s*-{0,2}([a-zA-Z0-9_-]+)\s*\)").unwrap());
339
340// Tailwind color palette - embedded from tailwind-colors.html
341static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> = Lazy::new(|| {
342    let mut colors = IndexMap::new();
343    
344    let mut slate = IndexMap::new();
345    slate.insert("50", "#f8fafc"); slate.insert("100", "#f1f5f9"); slate.insert("200", "#e2e8f0");
346    slate.insert("300", "#cbd5e1"); slate.insert("400", "#94a3b8"); slate.insert("500", "#64748b");
347    slate.insert("600", "#475569"); slate.insert("700", "#334155"); slate.insert("800", "#1e293b");
348    slate.insert("900", "#0f172a"); slate.insert("950", "#020617");
349    colors.insert("slate", slate);
350    
351    let mut gray = IndexMap::new();
352    gray.insert("50", "#f9fafb"); gray.insert("100", "#f3f4f6"); gray.insert("200", "#e5e7eb");
353    gray.insert("300", "#d1d5db"); gray.insert("400", "#9ca3af"); gray.insert("500", "#6b7280");
354    gray.insert("600", "#4b5563"); gray.insert("700", "#374151"); gray.insert("800", "#1f2937");
355    gray.insert("900", "#111827"); gray.insert("950", "#030712");
356    colors.insert("gray", gray);
357    
358    let mut zinc = IndexMap::new();
359    zinc.insert("50", "#fafafa"); zinc.insert("100", "#f4f4f5"); zinc.insert("200", "#e4e4e7");
360    zinc.insert("300", "#d4d4d8"); zinc.insert("400", "#a1a1aa"); zinc.insert("500", "#71717a");
361    zinc.insert("600", "#52525b"); zinc.insert("700", "#3f3f46"); zinc.insert("800", "#27272a");
362    zinc.insert("900", "#18181b"); zinc.insert("950", "#09090b");
363    colors.insert("zinc", zinc);
364    
365    let mut neutral = IndexMap::new();
366    neutral.insert("50", "#fafafa"); neutral.insert("100", "#f5f5f5"); neutral.insert("200", "#e5e5e5");
367    neutral.insert("300", "#d4d4d4"); neutral.insert("400", "#a3a3a3"); neutral.insert("500", "#737373");
368    neutral.insert("600", "#525252"); neutral.insert("700", "#404040"); neutral.insert("800", "#262626");
369    neutral.insert("900", "#171717"); neutral.insert("950", "#0a0a0a");
370    colors.insert("neutral", neutral);
371    
372    let mut stone = IndexMap::new();
373    stone.insert("50", "#fafaf9"); stone.insert("100", "#f5f5f4"); stone.insert("200", "#e7e5e4");
374    stone.insert("300", "#d6d3d1"); stone.insert("400", "#a8a29e"); stone.insert("500", "#78716c");
375    stone.insert("600", "#57534e"); stone.insert("700", "#44403c"); stone.insert("800", "#292524");
376    stone.insert("900", "#1c1917"); stone.insert("950", "#0c0a09");
377    colors.insert("stone", stone);
378    
379    let mut red = IndexMap::new();
380    red.insert("50", "#fef2f2"); red.insert("100", "#fee2e2"); red.insert("200", "#fecaca");
381    red.insert("300", "#fca5a5"); red.insert("400", "#f87171"); red.insert("500", "#ef4444");
382    red.insert("600", "#dc2626"); red.insert("700", "#b91c1c"); red.insert("800", "#991b1b");
383    red.insert("900", "#7f1d1d"); red.insert("950", "#450a0a");
384    colors.insert("red", red);
385    
386    let mut orange = IndexMap::new();
387    orange.insert("50", "#fff7ed"); orange.insert("100", "#ffedd5"); orange.insert("200", "#fed7aa");
388    orange.insert("300", "#fdba74"); orange.insert("400", "#fb923c"); orange.insert("500", "#f97316");
389    orange.insert("600", "#ea580c"); orange.insert("700", "#c2410c"); orange.insert("800", "#9a3412");
390    orange.insert("900", "#7c2d12"); orange.insert("950", "#431407");
391    colors.insert("orange", orange);
392    
393    let mut amber = IndexMap::new();
394    amber.insert("50", "#fffbeb"); amber.insert("100", "#fef3c7"); amber.insert("200", "#fde68a");
395    amber.insert("300", "#fcd34d"); amber.insert("400", "#fbbf24"); amber.insert("500", "#f59e0b");
396    amber.insert("600", "#d97706"); amber.insert("700", "#b45309"); amber.insert("800", "#92400e");
397    amber.insert("900", "#78350f"); amber.insert("950", "#451a03");
398    colors.insert("amber", amber);
399    
400    let mut yellow = IndexMap::new();
401    yellow.insert("50", "#fefce8"); yellow.insert("100", "#fef9c3"); yellow.insert("200", "#fef08a");
402    yellow.insert("300", "#fde047"); yellow.insert("400", "#facc15"); yellow.insert("500", "#eab308");
403    yellow.insert("600", "#ca8a04"); yellow.insert("700", "#a16207"); yellow.insert("800", "#854d0e");
404    yellow.insert("900", "#713f12"); yellow.insert("950", "#422006");
405    colors.insert("yellow", yellow);
406    
407    let mut lime = IndexMap::new();
408    lime.insert("50", "#f7fee7"); lime.insert("100", "#ecfccb"); lime.insert("200", "#d9f99d");
409    lime.insert("300", "#bef264"); lime.insert("400", "#a3e635"); lime.insert("500", "#84cc16");
410    lime.insert("600", "#65a30d"); lime.insert("700", "#4d7c0f"); lime.insert("800", "#3f6212");
411    lime.insert("900", "#365314"); lime.insert("950", "#1a2e05");
412    colors.insert("lime", lime);
413    
414    let mut green = IndexMap::new();
415    green.insert("50", "#f0fdf4"); green.insert("100", "#dcfce7"); green.insert("200", "#bbf7d0");
416    green.insert("300", "#86efac"); green.insert("400", "#4ade80"); green.insert("500", "#22c55e");
417    green.insert("600", "#16a34a"); green.insert("700", "#15803d"); green.insert("800", "#166534");
418    green.insert("900", "#14532d"); green.insert("950", "#052e16");
419    colors.insert("green", green);
420    
421    let mut emerald = IndexMap::new();
422    emerald.insert("50", "#ecfdf5"); emerald.insert("100", "#d1fae5"); emerald.insert("200", "#a7f3d0");
423    emerald.insert("300", "#6ee7b7"); emerald.insert("400", "#34d399"); emerald.insert("500", "#10b981");
424    emerald.insert("600", "#059669"); emerald.insert("700", "#047857"); emerald.insert("800", "#065f46");
425    emerald.insert("900", "#064e3b"); emerald.insert("950", "#022c22");
426    colors.insert("emerald", emerald);
427    
428    let mut teal = IndexMap::new();
429    teal.insert("50", "#f0fdfa"); teal.insert("100", "#ccfbf1"); teal.insert("200", "#99f6e4");
430    teal.insert("300", "#5eead4"); teal.insert("400", "#2dd4bf"); teal.insert("500", "#14b8a6");
431    teal.insert("600", "#0d9488"); teal.insert("700", "#0f766e"); teal.insert("800", "#115e59");
432    teal.insert("900", "#134e4a"); teal.insert("950", "#042f2e");
433    colors.insert("teal", teal);
434    
435    let mut cyan = IndexMap::new();
436    cyan.insert("50", "#ecfeff"); cyan.insert("100", "#cffafe"); cyan.insert("200", "#a5f3fc");
437    cyan.insert("300", "#67e8f9"); cyan.insert("400", "#22d3ee"); cyan.insert("500", "#06b6d4");
438    cyan.insert("600", "#0891b2"); cyan.insert("700", "#0e7490"); cyan.insert("800", "#155e75");
439    cyan.insert("900", "#164e63"); cyan.insert("950", "#083344");
440    colors.insert("cyan", cyan);
441    
442    let mut sky = IndexMap::new();
443    sky.insert("50", "#f0f9ff"); sky.insert("100", "#e0f2fe"); sky.insert("200", "#bae6fd");
444    sky.insert("300", "#7dd3fc"); sky.insert("400", "#38bdf8"); sky.insert("500", "#0ea5e9");
445    sky.insert("600", "#0284c7"); sky.insert("700", "#0369a1"); sky.insert("800", "#075985");
446    sky.insert("900", "#0c4a6e"); sky.insert("950", "#082f49");
447    colors.insert("sky", sky);
448    
449    let mut blue = IndexMap::new();
450    blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
451    blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
452    blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
453    blue.insert("900", "#1e3a8a"); blue.insert("950", "#172554");
454    colors.insert("blue", blue);
455    
456    let mut indigo = IndexMap::new();
457    indigo.insert("50", "#eef2ff"); indigo.insert("100", "#e0e7ff"); indigo.insert("200", "#c7d2fe");
458    indigo.insert("300", "#a5b4fc"); indigo.insert("400", "#818cf8"); indigo.insert("500", "#6366f1");
459    indigo.insert("600", "#4f46e5"); indigo.insert("700", "#4338ca"); indigo.insert("800", "#3730a3");
460    indigo.insert("900", "#312e81"); indigo.insert("950", "#1e1b4b");
461    colors.insert("indigo", indigo);
462    
463    let mut violet = IndexMap::new();
464    violet.insert("50", "#f5f3ff"); violet.insert("100", "#ede9fe"); violet.insert("200", "#ddd6fe");
465    violet.insert("300", "#c4b5fd"); violet.insert("400", "#a78bfa"); violet.insert("500", "#8b5cf6");
466    violet.insert("600", "#7c3aed"); violet.insert("700", "#6d28d9"); violet.insert("800", "#5b21b6");
467    violet.insert("900", "#4c1d95"); violet.insert("950", "#2e1065");
468    colors.insert("violet", violet);
469    
470    let mut purple = IndexMap::new();
471    purple.insert("50", "#faf5ff"); purple.insert("100", "#f3e8ff"); purple.insert("200", "#e9d5ff");
472    purple.insert("300", "#d8b4fe"); purple.insert("400", "#c084fc"); purple.insert("500", "#a855f7");
473    purple.insert("600", "#9333ea"); purple.insert("700", "#7e22ce"); purple.insert("800", "#6b21a8");
474    purple.insert("900", "#581c87"); purple.insert("950", "#3b0764");
475    colors.insert("purple", purple);
476    
477    let mut fuchsia = IndexMap::new();
478    fuchsia.insert("50", "#fdf4ff"); fuchsia.insert("100", "#fae8ff"); fuchsia.insert("200", "#f5d0fe");
479    fuchsia.insert("300", "#f0abfc"); fuchsia.insert("400", "#e879f9"); fuchsia.insert("500", "#d946ef");
480    fuchsia.insert("600", "#c026d3"); fuchsia.insert("700", "#a21caf"); fuchsia.insert("800", "#86198f");
481    fuchsia.insert("900", "#701a75"); fuchsia.insert("950", "#4a044e");
482    colors.insert("fuchsia", fuchsia);
483    
484    let mut pink = IndexMap::new();
485    pink.insert("50", "#fdf2f8"); pink.insert("100", "#fce7f3"); pink.insert("200", "#fbcfe8");
486    pink.insert("300", "#f9a8d4"); pink.insert("400", "#f472b6"); pink.insert("500", "#ec4899");
487    pink.insert("600", "#db2777"); pink.insert("700", "#be185d"); pink.insert("800", "#9d174d");
488    pink.insert("900", "#831843"); pink.insert("950", "#500724");
489    colors.insert("pink", pink);
490    
491    let mut rose = IndexMap::new();
492    rose.insert("50", "#fff1f2"); rose.insert("100", "#ffe4e6"); rose.insert("200", "#fecdd3");
493    rose.insert("300", "#fda4af"); rose.insert("400", "#fb7185"); rose.insert("500", "#f43f5e");
494    rose.insert("600", "#e11d48"); rose.insert("700", "#be123c"); rose.insert("800", "#9f1239");
495    rose.insert("900", "#881337"); rose.insert("950", "#4c0519");
496    colors.insert("rose", rose);
497    
498    colors
499});
500
501fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
502    let mut out = input.to_string();
503    for cap in RE_VAR.captures_iter(input) {
504        if let Some(name) = cap.get(1) {
505            let key = name.as_str();
506            if let Some(val) = vars.get(key) {
507                // replace both var(--name) and var(name)
508                out = out.replace(&format!("var(--{})", key), val);
509                out = out.replace(&format!("var({})", key), val);
510            }
511        }
512    }
513    if out.starts_with('$') {
514        if let Some(val) = vars.get(&out[1..]) {
515            return val.clone();
516        }
517    }
518    out
519}
520
521fn camel_case(name: &str) -> String {
522    let mut out = String::new();
523    let mut upper = false;
524    for ch in name.chars() {
525        if ch == '-' {
526            upper = true;
527            continue;
528        }
529        if upper {
530            out.extend(ch.to_uppercase());
531            upper = false;
532        } else {
533            out.push(ch);
534        }
535    }
536    out
537}
538
539fn css_value_to_rn(
540    value: &serde_json::Value,
541    vars: &IndexMap<String, String>,
542) -> serde_json::Value {
543    match value {
544        serde_json::Value::String(s) => {
545            let s2 = resolve_vars(s, vars);
546            if let Some(n) = s2.strip_suffix("px") {
547                if let Ok(parsed) = n.trim().parse::<f64>() {
548                    return json!(parsed);
549                }
550            }
551            json!(s2)
552        }
553        _ => value.clone(),
554    }
555}
556
557fn merge_rn_props(
558    into: &mut IndexMap<String, serde_json::Value>,
559    css_props: &CssProps,
560    vars: &IndexMap<String, String>,
561) {
562    for (k, v) in css_props.iter() {
563        let rn_key = match k.as_str() {
564            // Minimal explicit mappings
565            "background-color" => "backgroundColor".to_string(),
566            "text-align" => "textAlign".to_string(),
567            _ => camel_case(k),
568        };
569        let rn_val = css_value_to_rn(v, vars);
570        into.insert(rn_key, rn_val);
571    }
572}
573
574fn dynamic_css_properties_for_class(class: &str, vars: &IndexMap<String, String>) -> Option<CssProps> {
575    // Display utilities
576    match class {
577        "block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("block")); return Some(p); }
578        "inline-block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-block")); return Some(p); }
579        "inline" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline")); return Some(p); }
580        "inline-flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-flex")); return Some(p); }
581        "grid" => { let mut p = CssProps::new(); p.insert("display".into(), json!("grid")); return Some(p); }
582        "hidden" => { let mut p = CssProps::new(); p.insert("display".into(), json!("none")); return Some(p); }
583        _ => {}
584    }
585    // Flexbox shorthands
586    match class {
587        "flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); return Some(p); }
588        "flex-row" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("row")); return Some(p); }
589        "flex-col" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("column")); return Some(p); }
590        "flex-1" => { let mut p = CssProps::new(); p.insert("flex".into(), json!(1)); return Some(p); }
591        _ => {}
592    }
593    if let Some(rest) = class.strip_prefix("items-") {
594        let mut p = CssProps::new();
595        let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "stretch" => "stretch", other => other };
596        p.insert("align-items".into(), json!(v));
597        return Some(p);
598    }
599    if let Some(rest) = class.strip_prefix("justify-") {
600        let mut p = CssProps::new();
601        let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "between" => "space-between", "around" => "space-around", "evenly" => "space-evenly", other => other };
602        p.insert("justify-content".into(), json!(v));
603        return Some(p);
604    }
605    if let Some(value) = class.strip_prefix("p-") {
606        return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
607    }
608    if let Some(value) = class.strip_prefix("px-") {
609        return parse_tailwind_spacing(value, &|px| padding_props(&["padding-left", "padding-right"], px));
610    }
611    if let Some(value) = class.strip_prefix("py-") {
612        return parse_tailwind_spacing(value, &|px| padding_props(&["padding-top", "padding-bottom"], px));
613    }
614    for &(prefix, prop) in &[("pt-", "padding-top"), ("pr-", "padding-right"), ("pb-", "padding-bottom"), ("pl-", "padding-left")] {
615        if let Some(value) = class.strip_prefix(prefix) {
616            return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
617        }
618    }
619    // Margin utilities
620    if let Some(value) = class.strip_prefix("m-") {
621        return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
622    }
623    if let Some(value) = class.strip_prefix("mx-") {
624        return parse_tailwind_spacing(value, &|px| margin_props(&["margin-left", "margin-right"], px));
625    }
626    if let Some(value) = class.strip_prefix("my-") {
627        return parse_tailwind_spacing(value, &|px| margin_props(&["margin-top", "margin-bottom"], px));
628    }
629    for &(prefix, prop) in &[("mt-", "margin-top"), ("mr-", "margin-right"), ("mb-", "margin-bottom"), ("ml-", "margin-left")] {
630        if let Some(value) = class.strip_prefix(prefix) {
631            return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
632        }
633    }
634    // Gap utilities (works in RN 0.71+ with Flexbox)
635    if let Some(value) = class.strip_prefix("gap-") {
636        if !value.starts_with("x-") && !value.starts_with("y-") {
637            return parse_tailwind_spacing(value, &|px| {
638                let mut props = CssProps::new();
639                props.insert("gap".into(), json!(format!("{}px", px)));
640                props
641            });
642        }
643    }
644    if let Some(value) = class.strip_prefix("gap-x-") {
645        return parse_tailwind_spacing(value, &|px| {
646            let mut props = CssProps::new();
647            props.insert("column-gap".into(), json!(format!("{}px", px)));
648            props
649        });
650    }
651    if let Some(value) = class.strip_prefix("gap-y-") {
652        return parse_tailwind_spacing(value, &|px| {
653            let mut props = CssProps::new();
654            props.insert("row-gap".into(), json!(format!("{}px", px)));
655            props
656        });
657    }
658    // Space utilities (space-x-*, space-y-*)
659    if let Some(value) = class.strip_prefix("space-x-") {
660        return parse_tailwind_spacing(value, &|px| {
661            let mut props = CssProps::new();
662            // In CSS, this is typically done with :not(:last-child) selector
663            // For now, we'll set it as a custom property that can be used
664            props.insert("--space-x".into(), json!(format!("{}px", px)));
665            props
666        });
667    }
668    if let Some(value) = class.strip_prefix("space-y-") {
669        return parse_tailwind_spacing(value, &|px| {
670            let mut props = CssProps::new();
671            props.insert("--space-y".into(), json!(format!("{}px", px)));
672            props
673        });
674    }
675    // Font weight utilities
676    match class {
677        "font-thin" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("100")); return Some(p); }
678        "font-extralight" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("200")); return Some(p); }
679        "font-light" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("300")); return Some(p); }
680        "font-normal" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("400")); return Some(p); }
681        "font-medium" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("500")); return Some(p); }
682        "font-semibold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("600")); return Some(p); }
683        "font-bold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("700")); return Some(p); }
684        "font-extrabold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("800")); return Some(p); }
685        "font-black" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("900")); return Some(p); }
686        _ => {}
687    }
688    // Font family utilities
689    match class {
690        "font-sans" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("system-ui, -apple-system, sans-serif")); return Some(p); }
691        "font-serif" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("Georgia, serif")); return Some(p); }
692        "font-mono" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("ui-monospace, monospace")); return Some(p); }
693        _ => {}
694    }
695    // Text size utilities
696    match class {
697        "text-xs" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("12px")); p.insert("line-height".into(), json!("16px")); return Some(p); }
698        "text-sm" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("14px")); p.insert("line-height".into(), json!("20px")); return Some(p); }
699        "text-base" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("16px")); p.insert("line-height".into(), json!("24px")); return Some(p); }
700        "text-lg" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("18px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
701        "text-xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("20px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
702        "text-2xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("24px")); p.insert("line-height".into(), json!("32px")); return Some(p); }
703        "text-3xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("30px")); p.insert("line-height".into(), json!("36px")); return Some(p); }
704        "text-4xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("36px")); p.insert("line-height".into(), json!("40px")); return Some(p); }
705        "text-5xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("48px")); p.insert("line-height".into(), json!("1")); return Some(p); }
706        "text-6xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("60px")); p.insert("line-height".into(), json!("1")); return Some(p); }
707        _ => {}
708    }
709    // Text alignment
710    match class {
711        "text-left" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("left")); return Some(p); }
712        "text-center" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("center")); return Some(p); }
713        "text-right" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("right")); return Some(p); }
714        "text-justify" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("justify")); return Some(p); }
715        _ => {}
716    }
717    // Overflow utilities
718    match class {
719        "overflow-auto" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("auto")); return Some(p); }
720        "overflow-hidden" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("hidden")); return Some(p); }
721        "overflow-visible" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("visible")); return Some(p); }
722        "overflow-scroll" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("scroll")); return Some(p); }
723        "overflow-x-auto" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("auto")); return Some(p); }
724        "overflow-x-hidden" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("hidden")); return Some(p); }
725        "overflow-x-scroll" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("scroll")); return Some(p); }
726        "overflow-y-auto" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("auto")); return Some(p); }
727        "overflow-y-hidden" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("hidden")); return Some(p); }
728        "overflow-y-scroll" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("scroll")); return Some(p); }
729        _ => {}
730    }
731    // Shadow utilities (basic cross-platform support)
732    match class {
733        "shadow-sm" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 1px 2px 0 rgba(0, 0, 0, 0.05)")); return Some(p); }
734        "shadow" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)")); return Some(p); }
735        "shadow-md" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)")); return Some(p); }
736        "shadow-lg" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)")); return Some(p); }
737        "shadow-xl" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)")); return Some(p); }
738        "shadow-2xl" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 25px 50px -12px rgba(0, 0, 0, 0.25)")); return Some(p); }
739        "shadow-none" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("none")); return Some(p); }
740        _ => {}
741    }
742    // Parse arbitrary values like bg-[var(--primary)], text-[#ff0000], etc.
743    if let Some(arb_value) = parse_arbitrary_value(class) {
744        return Some(arb_value);
745    }
746    // text-{color}-{shade}
747    if let Some(rest) = class.strip_prefix("text-") {
748        if let Some(hex) = get_tailwind_color(rest) {
749            let mut props = CssProps::new();
750            props.insert("color".into(), json!(hex));
751            return Some(props);
752        }
753    }
754    // bg-{color}-{shade}
755    if let Some(rest) = class.strip_prefix("bg-") {
756        if let Some(hex) = get_tailwind_color(rest) {
757            let mut props = CssProps::new();
758            props.insert("background-color".into(), json!(hex));
759            return Some(props);
760        }
761    }
762    // divide-{color}-{shade} (sets border-color for child dividers)
763    if let Some(rest) = class.strip_prefix("divide-") {
764        if let Some(hex) = get_tailwind_color(rest) {
765            let mut props = CssProps::new();
766            props.insert("border-color".into(), json!(hex));
767            return Some(props);
768        }
769    }
770    if class == "border" {
771        return Some(border_props(None, 1, vars));
772    }
773    if let Some(rest) = class.strip_prefix("border-") {
774        // Check if it's a color first (e.g., border-slate-200)
775        if let Some(hex) = get_tailwind_color(rest) {
776            let mut props = CssProps::new();
777            props.insert("border-color".into(), json!(hex));
778            return Some(props);
779        }
780        // Otherwise, check for width/side (e.g., border-2, border-t-4)
781        let parts: Vec<&str> = rest.split('-').collect();
782        if parts.is_empty() {
783            return None;
784        }
785        let width_part = parts.last().unwrap();
786        if let Ok(width) = width_part.parse::<i32>() {
787            let side = if parts.len() == 2 { Some(parts[0]) } else { None };
788            return Some(border_props(side, width, vars));
789        }
790    }
791    // rounded* (border-radius)
792    if class == "rounded" { return Some(rounded_props(None, Some("md"))); }
793    if let Some(sz) = class.strip_prefix("rounded-") {
794        return Some(rounded_props(None, Some(sz)));
795    }
796    for &(pref, side) in &[("rounded-t", "t"), ("rounded-b", "b"), ("rounded-l", "l"), ("rounded-r", "r")] {
797        if class == pref { return Some(rounded_props(Some(side), Some("md"))); }
798        if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
799            return Some(rounded_props(Some(side), Some(sz)));
800        }
801    }
802    // cursor-*
803    if let Some(cur) = class.strip_prefix("cursor-") {
804        let mut props = CssProps::new();
805        props.insert("cursor".into(), json!(match cur {
806            "pointer" => "pointer",
807            "default" => "default",
808            "text" => "text",
809            "move" => "move",
810            "wait" => "wait",
811            "not-allowed" => "not-allowed",
812            other => other,
813        }));
814        return Some(props);
815    }
816    // transition*
817    if class == "transition" || class == "transition-all" {
818        let mut props = CssProps::new();
819        props.insert("transition-property".into(), json!("all"));
820        props.insert("transition-duration".into(), json!("150ms"));
821        props.insert("transition-timing-function".into(), json!("ease-in-out"));
822        return Some(props);
823    }
824    if class == "transition-none" {
825        let mut props = CssProps::new();
826        props.insert("transition-property".into(), json!("none"));
827        props.insert("transition-duration".into(), json!("0ms"));
828        return Some(props);
829    }
830    if let Some(rest) = class.strip_prefix("transition-") {
831        // e.g., transition-colors → limit property; keep default duration/ease
832        let mut props = CssProps::new();
833        let property = match rest {
834            "colors" => "color, background-color, border-color, fill, stroke",
835            "opacity" => "opacity",
836            "transform" => "transform",
837            "shadow" => "box-shadow",
838            other => other,
839        };
840        props.insert("transition-property".into(), json!(property));
841        props.insert("transition-duration".into(), json!("150ms"));
842        props.insert("transition-timing-function".into(), json!("ease-in-out"));
843        return Some(props);
844    }
845    // width utilities: w-*, w-full, w-screen, w-min, w-max (treat min/max as auto), w-px
846    if let Some(val) = class.strip_prefix("w-") {
847        return width_like_props("width", val);
848    }
849    if let Some(val) = class.strip_prefix("min-w-") {
850        return width_like_props("min-width", val);
851    }
852    if let Some(val) = class.strip_prefix("max-w-") {
853        return width_like_props("max-width", val);
854    }
855    // Height utilities
856    if let Some(val) = class.strip_prefix("h-") {
857        return width_like_props("height", val);
858    }
859    if let Some(val) = class.strip_prefix("min-h-") {
860        return width_like_props("min-height", val);
861    }
862    if let Some(val) = class.strip_prefix("max-h-") {
863        return width_like_props("max-height", val);
864    }
865    None
866}
867
868fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
869where
870    F: Fn(i32) -> CssProps,
871{
872    if let Ok(n) = value.parse::<i32>() {
873        let px = n * 4;
874        return Some(builder(px));
875    }
876    None
877}
878
879fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
880    let mut props = CssProps::new();
881    let val = format!("{}px", px_value);
882    for key in keys {
883        props.insert((*key).into(), json!(&val));
884    }
885    props
886}
887
888fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
889    let mut props = CssProps::new();
890    let val = format!("{}px", px_value);
891    for key in keys {
892        props.insert((*key).into(), json!(&val));
893    }
894    props
895}
896
897fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
898    let mut props = CssProps::new();
899    let width_str = format!("{}px", width);
900    match side {
901        None => {
902            props.insert("border-width".into(), json!(&width_str));
903        }
904        Some("t") => {
905            props.insert("border-top-width".into(), json!(&width_str));
906        }
907        Some("b") => {
908            props.insert("border-bottom-width".into(), json!(&width_str));
909        }
910        Some("l") => {
911            props.insert("border-left-width".into(), json!(&width_str));
912        }
913        Some("r") => {
914            props.insert("border-right-width".into(), json!(&width_str));
915        }
916        Some("x") => {
917            props.insert("border-left-width".into(), json!(&width_str));
918            props.insert("border-right-width".into(), json!(&width_str));
919        }
920        Some("y") => {
921            props.insert("border-top-width".into(), json!(&width_str));
922            props.insert("border-bottom-width".into(), json!(&width_str));
923        }
924        _ => {
925            props.insert("border-width".into(), json!(&width_str));
926        }
927    };
928    props.insert("border-color".into(), json!("var(border)"));
929    props.insert("border-style".into(), json!("solid"));
930    props
931}
932
933fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
934    let mut props = CssProps::new();
935    let px = match size.unwrap_or("md") {
936        "none" => 0,
937        "sm" => 2,
938        "md" => 4,
939        "lg" => 8,
940        "xl" => 12,
941        "2xl" => 16,
942        "3xl" => 24,
943        "full" => 9999,
944        s => s.parse::<i32>().unwrap_or(4),
945    };
946    let v = json!(format!("{}px", px));
947    match side {
948        None => { props.insert("border-radius".into(), v); }
949        Some("t") => {
950            props.insert("border-top-left-radius".into(), v.clone());
951            props.insert("border-top-right-radius".into(), v);
952        }
953        Some("b") => {
954            props.insert("border-bottom-left-radius".into(), v.clone());
955            props.insert("border-bottom-right-radius".into(), v);
956        }
957        Some("l") => { props.insert("border-top-left-radius".into(), v.clone()); props.insert("border-bottom-left-radius".into(), v); }
958        Some("r") => { props.insert("border-top-right-radius".into(), v.clone()); props.insert("border-bottom-right-radius".into(), v); }
959        _ => { props.insert("border-radius".into(), v); }
960    }
961    props
962}
963
964fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
965    let mut props = CssProps::new();
966    let value = match token {
967        "full" => Some("100%".to_string()),
968        "screen" => Some(if prop == "width" { "100vw" } else { "100%" }.to_string()),
969        "min" => Some("min-content".to_string()),
970        "max" => Some("max-content".to_string()),
971        "fit" => Some("fit-content".to_string()),
972        "auto" => Some("auto".to_string()),
973        "px" => Some("1px".to_string()),
974        other => {
975            // numeric scale n => n*4px, fraction e.g., 1/2 => 50%
976            if let Some((a, b)) = other.split_once('/') {
977                if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
978                    let pct = (na / nb) * 100.0;
979                    Some(format!("{}%", trim_trailing_zeros(pct)))
980                } else { None }
981            } else if let Ok(n) = other.parse::<i32>() {
982                Some(format!("{}px", n * 4))
983            } else {
984                None
985            }
986        }
987    }?;
988    props.insert(prop.into(), json!(value));
989    Some(props)
990}
991
992fn trim_trailing_zeros(num: f64) -> String {
993    let mut s = format!("{:.6}", num);
994    while s.contains('.') && s.ends_with('0') { s.pop(); }
995    if s.ends_with('.') { s.pop(); }
996    s
997}
998
999// ---------------- Tailwind subset ----------------
1000
1001// static RE_NUM: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(?P<prefix>(hover:)?(xs:|sm:|md:|lg:|xl:)*)?(?P<base>.+)$").unwrap());
1002
1003fn css_escape_class(class: &str) -> String { class.replace(':', "\\:") }
1004
1005fn class_to_selector(class: &str) -> String {
1006    let (_bp, hover, base) = parse_prefixed_class(class);
1007    if hover {
1008        format!(".{}:hover", css_escape_class(&base))
1009    } else {
1010        format!(".{}", css_escape_class(&base))
1011    }
1012}
1013
1014// ------------- helpers for CSS output of media selectors -------------
1015
1016/// Flatten CSS with potential selectors that include media prelude.
1017/// This simple post-processor merges entries that use the special selector format
1018/// "@media (min-width: X) {<sel>" where we will close the block at the end.
1019/// We group by media and inside concatenate selectors.
1020pub fn post_process_css(
1021    raw_rules: &[(String, CssProps)],
1022    vars: &IndexMap<String, String>,
1023) -> String {
1024    // Group into normal rules and media rules
1025    let mut normal = vec![];
1026    let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
1027    for (sel, props) in raw_rules.iter() {
1028        if let Some((media, inner)) = sel.split_once('{') {
1029            if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
1030                let inner_sel = inner.trim_end_matches('}').to_string();
1031                media_map
1032                    .entry(media.trim().to_string())
1033                    .or_default()
1034                    .push((inner_sel, props.clone()));
1035                continue;
1036            }
1037        }
1038        normal.push((sel.clone(), props.clone()));
1039    }
1040    let mut out = String::new();
1041    for (sel, props) in normal {
1042        out.push_str(&sel);
1043        out.push('{');
1044        out.push_str(&css_props_string(&props, vars));
1045        out.push_str("}\n");
1046    }
1047    for (media, entries) in media_map {
1048        out.push_str(&media);
1049        out.push('{');
1050        for (sel, props) in entries {
1051            out.push_str(&sel);
1052            out.push('{');
1053            out.push_str(&css_props_string(&props, vars));
1054            out.push_str("}");
1055        }
1056        out.push_str("}\n");
1057    }
1058    out
1059}
1060
1061// -------- Prefix parsing (hover:, breakpoint:) --------
1062
1063fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1064    // Split by ':' to find prefixes like md:hover:block
1065    let parts: Vec<&str> = class.split(':').collect();
1066    if parts.len() == 1 {
1067        return (None, false, class.to_string());
1068    }
1069    let mut bp: Option<String> = None;
1070    let mut hover = false;
1071    for &p in &parts[..parts.len() - 1] {
1072        match p {
1073            "hover" => hover = true,
1074            "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
1075            _ => {}
1076        }
1077    }
1078    let base = parts.last().unwrap().to_string();
1079    (bp, hover, base)
1080}
1081
1082fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
1083    if let Some(k) = bp_key {
1084        if let Some(val) = bps.get(k) {
1085            return format!("@media (min-width: {}) {{{}}}", val, selector);
1086        }
1087    }
1088    selector.to_string()
1089}
1090
1091/// Get a Tailwind color hex value from a string like "slate-200" or "blue-500"
1092fn get_tailwind_color(color_shade: &str) -> Option<String> {
1093    let parts: Vec<&str> = color_shade.split('-').collect();
1094    if parts.len() != 2 {
1095        return None;
1096    }
1097    let color_name = parts[0];
1098    let shade = parts[1];
1099    
1100    TAILWIND_COLORS
1101        .get(color_name)
1102        .and_then(|shades| shades.get(shade))
1103        .map(|&hex| hex.to_string())
1104}
1105
1106/// Parse arbitrary values like bg-[var(--primary)], text-[#ff0000], border-[hsl(200,50%,50%)]
1107fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
1108    // Match pattern: prefix-[value]
1109    if let Some(bracket_start) = class.find('[') {
1110        if !class.ends_with(']') {
1111            return None;
1112        }
1113        let prefix = &class[..bracket_start];
1114        let value = &class[bracket_start + 1..class.len() - 1];
1115        
1116        let mut props = CssProps::new();
1117        match prefix {
1118            "bg" => {
1119                props.insert("background-color".into(), json!(value));
1120                return Some(props);
1121            }
1122            "text" => {
1123                props.insert("color".into(), json!(value));
1124                return Some(props);
1125            }
1126            "border" => {
1127                props.insert("border-color".into(), json!(value));
1128                return Some(props);
1129            }
1130            "divide" => {
1131                props.insert("border-color".into(), json!(value));
1132                return Some(props);
1133            }
1134            _ => return None,
1135        }
1136    }
1137    None
1138}
1139
1140// re-export minimal API for CLI
1141pub mod api {
1142    pub use super::{SelectorStyles, State};
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147    use super::*;
1148
1149    #[test]
1150    fn default_theme_has_p2() {
1151        let mut st = State::new_default();
1152        let css = st.css_for_web();
1153        assert!(css.contains(".p-2{"));
1154        assert!(css.contains("padding:8px"));
1155    }
1156
1157    #[test]
1158    fn rn_conversion() {
1159        let st = State::new_default();
1160        let out = st.rn_styles_for("button", &[]);
1161        assert!(out.get("backgroundColor").is_some());
1162    }
1163
1164    #[test]
1165    fn embedded_defaults_and_version() {
1166        // default_state should contain the default theme and some variables
1167        let st = State::default_state();
1168        assert!(st.themes.contains_key("default"));
1169        let def = st.themes.get("default").unwrap();
1170        assert!(def.variables.contains_key("primary"));
1171
1172        // Version should compile and be non-empty (env! evaluated at compile-time)
1173        let v = get_version();
1174        assert!(!v.trim().is_empty());
1175    }
1176
1177    #[test]
1178    fn display_flex_hover_breakpoint() {
1179        let mut st = State::new_default();
1180        st.register_tailwind_classes([
1181            "block".into(),
1182            "inline-flex".into(),
1183            "hidden".into(),
1184            "md:flex".into(),
1185            "md:hover:block".into(),
1186        ]);
1187        let css = st.css_for_web();
1188        assert!(css.contains(".block{"));
1189        assert!(css.contains("display:block"));
1190        assert!(css.contains(".inline-flex{"));
1191        assert!(css.contains("display:inline-flex"));
1192        assert!(css.contains(".hidden{"));
1193        assert!(css.contains("display:none"));
1194        // breakpoint rule
1195        assert!(css.contains("@media (min-width: 768px)"));
1196        assert!(css.contains(".flex{display:flex"));
1197        // hover inside media (substring check)
1198        assert!(css.contains(":hover{display:block"));
1199
1200        // RN resolves base class styles ignoring prefixes
1201        let rn = st.rn_styles_for("div", &["md:flex".into()]);
1202        assert_eq!(rn.get("display").and_then(|v| v.as_str()), Some("flex"));
1203    }
1204}
1205
1206#[cfg(all(target_os = "android", feature = "android"))]
1207mod android_jni;
1208
1209#[cfg(target_vendor = "apple")]
1210mod ios_ffi;