1use indexmap::{IndexMap, IndexSet};
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{de::Deserializer, Deserialize, Serialize};
5use serde_json::json;
6#[cfg(target_arch = "wasm32")]
7use wasm_bindgen::prelude::*;
8mod default_state;
9use default_state::bundled_state;
10
11pub type CssProps = IndexMap<String, serde_json::Value>;
12pub type SelectorStyles = IndexMap<String, CssProps>; fn deserialize_variables<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
15where
16 D: Deserializer<'de>,
17{
18 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
19 let mut out: IndexMap<String, String> = IndexMap::new();
20 if let Some(v) = value {
21 flatten_variables(None, &v, &mut out);
22 }
23 Ok(out)
24}
25
26fn flatten_variables(prefix: Option<&str>, value: &serde_json::Value, out: &mut IndexMap<String, String>) {
27 match value {
28 serde_json::Value::Object(map) => {
29 for (k, v) in map {
30 let key = if let Some(p) = prefix {
31 format!("{}.{}", p, k)
32 } else {
33 k.to_string()
34 };
35 flatten_variables(Some(&key), v, out);
36 }
37 }
38 serde_json::Value::Array(arr) => {
39 for (idx, v) in arr.iter().enumerate() {
40 let key = if let Some(p) = prefix {
41 format!("{}.{}", p, idx)
42 } else {
43 idx.to_string()
44 };
45 flatten_variables(Some(&key), v, out);
46 }
47 }
48 serde_json::Value::Null => {}
49 serde_json::Value::Bool(b) => {
50 if let Some(p) = prefix {
51 out.insert(p.to_string(), b.to_string());
52 }
53 }
54 serde_json::Value::Number(n) => {
55 if let Some(p) = prefix {
56 out.insert(p.to_string(), n.to_string());
57 }
58 }
59 serde_json::Value::String(s) => {
60 if let Some(p) = prefix {
61 out.insert(p.to_string(), s.clone());
62 }
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default)]
68pub struct ThemeEntry {
69 #[serde(default)]
70 pub name: Option<String>,
71 #[serde(default)]
72 pub inherits: Option<String>,
73 #[serde(default)]
74 pub selectors: SelectorStyles,
75 #[serde(default, deserialize_with = "deserialize_variables")]
76 pub variables: IndexMap<String, String>,
77 #[serde(default)]
78 pub breakpoints: IndexMap<String, String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, Default)]
82pub struct State {
83 pub themes: IndexMap<String, ThemeEntry>,
85 pub default_theme: String,
86 pub current_theme: String,
87 #[serde(default)]
89 pub theme_variables: IndexMap<String, IndexMap<String, String>>, #[serde(default)]
91 pub variables: IndexMap<String, String>, #[serde(default)]
93 pub breakpoints: IndexMap<String, String>, #[serde(default)]
95 pub used_selectors: IndexSet<String>, #[serde(default)]
97 pub used_classes: IndexSet<String>, #[serde(default)]
99 pub used_tags: IndexSet<String>, #[serde(default)]
102 pub used_tag_classes: IndexSet<String>,
103}
104
105#[derive(thiserror::Error, Debug)]
106pub enum Error {
107 #[error("theme not found: {0}")]
108 ThemeNotFound(String),
109}
110
111impl State {
112 pub fn new_default() -> Self {
113 return bundled_state();
115 }
116
117 pub fn default_state() -> Self {
119 bundled_state()
120 }
121
122 pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
123 let name = theme.into();
124 if !self.themes.contains_key(&name) {
125 return Err(Error::ThemeNotFound(name));
126 }
127 self.current_theme = name;
128 Ok(())
129 }
130
131 pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
132 let name = name.into();
133 let entry = self.themes.entry(name).or_default();
134 for (sel, props) in styles.into_iter() {
135 let e = entry.selectors.entry(sel).or_default();
136 merge_props(e, &props);
137 }
138 }
139
140 pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
141 let cur = self.current_theme.clone();
143 let entry = self.themes.entry(cur).or_default();
144 entry.variables = vars;
145 }
146
147 pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
148 let cur = self.current_theme.clone();
149 let entry = self.themes.entry(cur).or_default();
150 entry.breakpoints = map;
151 }
152
153 pub fn set_default_theme(&mut self, name: impl Into<String>) {
154 self.default_theme = name.into();
155 }
156
157 pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, selectors: I) {
158 for s in selectors {
159 self.used_selectors.insert(s);
160 }
161 }
162
163 pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
164 for c in classes {
165 self.used_classes.insert(c);
166 }
167 }
168
169 pub fn register_tags<I: IntoIterator<Item = String>>(&mut self, tags: I) {
170 for t in tags {
171 self.used_tags.insert(t);
172 }
173 }
174
175 pub fn register_tag_class(&mut self, tag: impl Into<String>, class_: impl Into<String>) {
176 let key = format!("{}|{}", tag.into(), class_.into());
177 self.used_tag_classes.insert(key);
178 }
179
180
181 pub fn clear_usage(&mut self) {
182 self.used_selectors.clear();
183 self.used_classes.clear();
184 self.used_tags.clear();
185 self.used_tag_classes.clear();
186 }
187
188 pub fn to_json(&self) -> serde_json::Value {
189 json!({
190 "themes": self.themes,
191 "default_theme": self.default_theme,
192 "current_theme": self.current_theme,
193 "theme_variables": self.theme_variables,
195 "variables": self.variables,
196 "breakpoints": self.breakpoints,
197 "used_selectors": self.used_selectors,
198 "used_classes": self.used_classes,
199 "used_tags": self.used_tags,
200 "used_tag_classes": self.used_tag_classes,
201 })
202 }
203
204 pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
205 let state: State = serde_json::from_value(value)?;
206 Ok(state)
207 }
208
209 pub fn css_for_web(&self) -> String {
210 let (eff, vars) = self.effective_theme_all();
212 let bps = self.effective_breakpoints();
213 let mut rules: Vec<(String, CssProps)> = Vec::new();
214
215 let mut used_tags: IndexSet<String> = self.used_tags.clone();
217 let mut used_classes: IndexSet<String> = self.used_classes.clone();
218 for key in &self.used_tag_classes {
219 if let Some((t, c)) = split_tag_class_key(key) {
220 used_tags.insert(t);
221 used_classes.insert(c);
222 }
223 }
224
225 for (sel, props) in eff.iter() {
231 if should_emit_selector(sel, &used_tags, &used_classes, &self.used_tag_classes) {
232 rules.push((sel.clone(), props.clone()));
233 }
234 }
235
236 for class in &used_classes {
238 let (bp_key, hover, base) = parse_prefixed_class(class);
239 let selector = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
240
241 if let Some(props) = eff.get(&selector) {
243 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
244 rules.push((final_sel, props.clone()));
245 continue;
246 }
247 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
249 let sel = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
250 let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
251 rules.push((final_sel, dynamic_props));
252 continue;
253 }
254 if let Some(props) = eff.get(&base) {
256 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
257 rules.push((final_sel, props.clone()));
258 }
259 }
260
261 post_process_css(&rules, &vars)
262 }
263
264 pub fn rn_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
265 let (eff, vars) = self.effective_theme_all();
266 let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
267 if let Some(props) = eff.get(selector) {
268 merge_rn_props(&mut out, props, &vars);
269 }
270 for class in classes {
271 let (_bp, _hover, base) = parse_prefixed_class(class);
272 let sel = class_to_selector(&base);
274 if let Some(props) = eff.get(&sel) {
275 merge_rn_props(&mut out, props, &vars);
276 continue;
277 }
278 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
280 merge_rn_props(&mut out, &dynamic_props, &vars);
281 continue;
282 }
283 if let Some(props) = eff.get(&base) {
284 merge_rn_props(&mut out, props, &vars);
285 }
286 }
287 out
288 }
289
290 fn theme_chain(&self) -> Vec<String> {
294 let mut chain = Vec::new();
295 let default_name = if self.themes.contains_key(&self.default_theme) {
297 self.default_theme.clone()
298 } else if let Some((k, _)) = self.themes.first() { k.clone() } else { return chain };
299 let mut current_name = if self.themes.contains_key(&self.current_theme) {
300 self.current_theme.clone()
301 } else { default_name.clone() };
302 let mut seen: IndexSet<String> = IndexSet::new();
304 while !seen.contains(¤t_name) {
305 seen.insert(current_name.clone());
306 chain.push(current_name.clone());
307 let inherits = self.themes.get(¤t_name).and_then(|t| t.inherits.clone());
309 if let Some(p) = inherits {
310 current_name = p;
311 } else {
312 break;
313 }
314 }
315 if !chain.iter().any(|n| n == &default_name) {
316 chain.push(default_name);
317 }
318 chain
319 }
320
321 fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
324 let mut selectors: SelectorStyles = SelectorStyles::new();
325 let mut vars: IndexMap<String, String> = IndexMap::new();
326 for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
328 let chain = self.theme_chain();
330 for name in chain.into_iter().rev() {
331 if let Some(entry) = self.themes.get(&name) {
332 for (sel, props) in entry.selectors.iter() {
334 let e = selectors.entry(sel.clone()).or_default();
335 merge_props(e, props);
336 }
337 for (k, v) in entry.variables.iter() {
339 vars.insert(k.clone(), v.clone());
340 }
341 }
342 }
343 (selectors, vars)
344 }
345
346 pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
348 let mut bps: IndexMap<String, String> = IndexMap::new();
349 for (k, v) in self.breakpoints.iter() { bps.insert(k.clone(), v.clone()); }
351 let chain = self.theme_chain();
352 for name in chain.into_iter().rev() {
353 if let Some(entry) = self.themes.get(&name) {
354 for (k, v) in entry.breakpoints.iter() {
355 bps.insert(k.clone(), v.clone());
356 }
357 }
358 }
359 bps
360 }
361}
362
363fn split_tag_class_key(key: &str) -> Option<(String, String)> {
364 let mut it = key.splitn(2, '|');
365 let t = it.next()?.to_string();
366 let c = it.next()?.to_string();
367 if t.is_empty() || c.is_empty() { return None; }
368 Some((t, c))
369}
370
371fn strip_hover_suffix(selector: &str) -> (&str, bool) {
372 if let Some(stripped) = selector.strip_suffix(":hover") { (stripped, true) } else { (selector, false) }
373}
374
375fn should_emit_selector(sel: &str, used_tags: &IndexSet<String>, used_classes: &IndexSet<String>, used_tag_classes: &IndexSet<String>) -> bool {
376 let (base, _hover) = strip_hover_suffix(sel);
378
379 if is_simple_tag(base) {
381 return used_tags.contains(base) || used_tag_classes.iter().any(|k| k.split('|').next() == Some(base));
382 }
383
384 if let Some(class_name) = base.strip_prefix('.') {
386 return used_classes.contains(class_name) || used_tag_classes.iter().any(|k| k.ends_with(&format!("|{}", class_name)));
388 }
389
390 if let Some((tag, class_name)) = split_tag_class_selector(base) {
392 let key = format!("{}|{}", tag, class_name);
393 return used_tag_classes.contains(&key) || (used_tags.contains(&tag) && used_classes.contains(&class_name));
394 }
395
396 false
398}
399
400fn is_simple_tag(s: &str) -> bool {
401 let mut chars = s.chars();
403 match chars.next() { Some(c) if c.is_ascii_alphabetic() => {}, _ => return false }
404 chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
405}
406
407fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
408 let mut parts = s.splitn(2, '.');
410 let tag = parts.next()?.to_string();
411 let class_name = parts.next()?.to_string();
412 if tag.is_empty() || class_name.is_empty() { return None; }
413 Some((tag, class_name))
414}
415
416#[cfg(target_arch = "wasm32")]
418#[wasm_bindgen]
419pub fn render_css_for_web(state_json: &str) -> String {
420 match serde_json::from_str::<State>(state_json) {
421 Ok(s) => s.css_for_web(),
422 Err(_) => "".into(),
423 }
424}
425
426#[cfg(target_arch = "wasm32")]
427#[wasm_bindgen]
428pub fn get_rn_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
429 let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
430 match serde_json::from_str::<State>(state_json) {
431 Ok(s) => serde_json::to_string(&s.rn_styles_for(selector, &classes)).unwrap_or_else(|_| "{}".into()),
432 Err(_) => "{}".into(),
433 }
434}
435
436#[cfg(target_arch = "wasm32")]
439#[wasm_bindgen]
440pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
441 get_rn_styles(state_json, selector, classes_json)
442}
443
444#[cfg(target_arch = "wasm32")]
446#[wasm_bindgen]
447pub fn get_version() -> String {
448 env!("CARGO_PKG_VERSION").to_string()
450}
451
452pub fn version() -> &'static str {
454 env!("CARGO_PKG_VERSION")
455}
456
457#[cfg(target_arch = "wasm32")]
459#[wasm_bindgen]
460pub fn get_default_state_json() -> String {
461 let st = bundled_state();
462 match serde_json::to_string(&st.to_json()) {
463 Ok(s) => s,
464 Err(_) => "{}".to_string(),
465 }
466}
467
468#[cfg(target_arch = "wasm32")]
472#[wasm_bindgen]
473pub fn register_theme_json(state_json: &str, theme_json: &str) -> String {
474 match (serde_json::from_str::<State>(state_json), serde_json::from_str::<serde_json::Value>(theme_json)) {
475 (Ok(mut state), Ok(theme_obj)) => {
476 if let (Some(name), Some(theme_entry)) = (theme_obj.get("name"), theme_obj.get("theme")) {
477 if let Ok(entry) = serde_json::from_value::<ThemeEntry>(theme_entry.clone()) {
478 let theme_name = name.as_str().unwrap_or("").to_string();
479 if !theme_name.is_empty() {
480 state.themes.insert(theme_name, entry);
481 }
482 }
483 }
484 match serde_json::to_string(&state.to_json()) {
485 Ok(s) => s,
486 Err(_) => "{}".to_string(),
487 }
488 }
489 _ => "{}".to_string(),
490 }
491}
492
493#[cfg(target_arch = "wasm32")]
495#[wasm_bindgen]
496pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
497 match serde_json::from_str::<State>(state_json) {
498 Ok(mut state) => {
499 if state.themes.contains_key(theme_name) {
500 state.default_theme = theme_name.to_string();
501 state.current_theme = theme_name.to_string();
502 }
503 match serde_json::to_string(&state.to_json()) {
504 Ok(s) => s,
505 Err(_) => "{}".to_string(),
506 }
507 }
508 _ => "{}".to_string(),
509 }
510}
511
512#[cfg(target_arch = "wasm32")]
515#[wasm_bindgen]
516pub fn get_theme_list_json(state_json: &str) -> String {
517 match serde_json::from_str::<State>(state_json) {
518 Ok(state) => {
519 let themes: Vec<serde_json::Value> = state.themes.iter().map(|(key, entry)| {
520 json!({
521 "key": key,
522 "name": entry.name.as_ref().unwrap_or(key)
523 })
524 }).collect();
525 serde_json::to_string(&themes).unwrap_or_else(|_| "[]".to_string())
526 }
527 _ => "[]".to_string(),
528 }
529}
530
531fn merge_props(into: &mut CssProps, from: &CssProps) {
532 for (k, v) in from.iter() {
533 into.insert(k.clone(), v.clone());
534 }
535}
536
537fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
540 let mut buf = String::new();
541 for (k, v) in props.iter() {
542 buf.push_str(k);
543 buf.push(':');
544 let val = if v.is_string() {
545 let s = v.as_str().unwrap();
546 resolve_vars(s, vars)
547 } else {
548 v.to_string()
549 };
550 buf.push_str(&val);
551 if !val.ends_with(';') {
552 buf.push(';');
553 }
554 }
555 buf
556}
557
558static RE_VAR: Lazy<Regex> = Lazy::new(|| Regex::new(r"var\(\s*-{0,2}([a-zA-Z0-9_.-]+)\s*\)").unwrap());
560
561static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> = Lazy::new(|| {
563 let mut colors = IndexMap::new();
564
565 let mut slate = IndexMap::new();
566 slate.insert("50", "#f8fafc"); slate.insert("100", "#f1f5f9"); slate.insert("200", "#e2e8f0");
567 slate.insert("300", "#cbd5e1"); slate.insert("400", "#94a3b8"); slate.insert("500", "#64748b");
568 slate.insert("600", "#475569"); slate.insert("700", "#334155"); slate.insert("800", "#1e293b");
569 slate.insert("900", "#0f172a"); slate.insert("950", "#020617");
570 colors.insert("slate", slate);
571
572 let mut gray = IndexMap::new();
573 gray.insert("50", "#f9fafb"); gray.insert("100", "#f3f4f6"); gray.insert("200", "#e5e7eb");
574 gray.insert("300", "#d1d5db"); gray.insert("400", "#9ca3af"); gray.insert("500", "#6b7280");
575 gray.insert("600", "#4b5563"); gray.insert("700", "#374151"); gray.insert("800", "#1f2937");
576 gray.insert("900", "#111827"); gray.insert("950", "#030712");
577 colors.insert("gray", gray);
578
579 let mut zinc = IndexMap::new();
580 zinc.insert("50", "#fafafa"); zinc.insert("100", "#f4f4f5"); zinc.insert("200", "#e4e4e7");
581 zinc.insert("300", "#d4d4d8"); zinc.insert("400", "#a1a1aa"); zinc.insert("500", "#71717a");
582 zinc.insert("600", "#52525b"); zinc.insert("700", "#3f3f46"); zinc.insert("800", "#27272a");
583 zinc.insert("900", "#18181b"); zinc.insert("950", "#09090b");
584 colors.insert("zinc", zinc);
585
586 let mut neutral = IndexMap::new();
587 neutral.insert("50", "#fafafa"); neutral.insert("100", "#f5f5f5"); neutral.insert("200", "#e5e5e5");
588 neutral.insert("300", "#d4d4d4"); neutral.insert("400", "#a3a3a3"); neutral.insert("500", "#737373");
589 neutral.insert("600", "#525252"); neutral.insert("700", "#404040"); neutral.insert("800", "#262626");
590 neutral.insert("900", "#171717"); neutral.insert("950", "#0a0a0a");
591 colors.insert("neutral", neutral);
592
593 let mut stone = IndexMap::new();
594 stone.insert("50", "#fafaf9"); stone.insert("100", "#f5f5f4"); stone.insert("200", "#e7e5e4");
595 stone.insert("300", "#d6d3d1"); stone.insert("400", "#a8a29e"); stone.insert("500", "#78716c");
596 stone.insert("600", "#57534e"); stone.insert("700", "#44403c"); stone.insert("800", "#292524");
597 stone.insert("900", "#1c1917"); stone.insert("950", "#0c0a09");
598 colors.insert("stone", stone);
599
600 let mut red = IndexMap::new();
601 red.insert("50", "#fef2f2"); red.insert("100", "#fee2e2"); red.insert("200", "#fecaca");
602 red.insert("300", "#fca5a5"); red.insert("400", "#f87171"); red.insert("500", "#ef4444");
603 red.insert("600", "#dc2626"); red.insert("700", "#b91c1c"); red.insert("800", "#991b1b");
604 red.insert("900", "#7f1d1d"); red.insert("950", "#450a0a");
605 colors.insert("red", red);
606
607 let mut orange = IndexMap::new();
608 orange.insert("50", "#fff7ed"); orange.insert("100", "#ffedd5"); orange.insert("200", "#fed7aa");
609 orange.insert("300", "#fdba74"); orange.insert("400", "#fb923c"); orange.insert("500", "#f97316");
610 orange.insert("600", "#ea580c"); orange.insert("700", "#c2410c"); orange.insert("800", "#9a3412");
611 orange.insert("900", "#7c2d12"); orange.insert("950", "#431407");
612 colors.insert("orange", orange);
613
614 let mut amber = IndexMap::new();
615 amber.insert("50", "#fffbeb"); amber.insert("100", "#fef3c7"); amber.insert("200", "#fde68a");
616 amber.insert("300", "#fcd34d"); amber.insert("400", "#fbbf24"); amber.insert("500", "#f59e0b");
617 amber.insert("600", "#d97706"); amber.insert("700", "#b45309"); amber.insert("800", "#92400e");
618 amber.insert("900", "#78350f"); amber.insert("950", "#451a03");
619 colors.insert("amber", amber);
620
621 let mut blue = IndexMap::new();
622 blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
623 blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
624 blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
625 blue.insert("900", "#1e3a8a"); blue.insert("950", "#0b1c52");
626 colors.insert("blue", blue);
627
628 let mut lime = IndexMap::new();
629 lime.insert("50", "#f7fee7"); lime.insert("100", "#ecfccb"); lime.insert("200", "#d9f99d");
630 lime.insert("300", "#bef264"); lime.insert("400", "#a3e635"); lime.insert("500", "#84cc16");
631 lime.insert("600", "#65a30d"); lime.insert("700", "#4d7c0f"); lime.insert("800", "#3f6212");
632 lime.insert("900", "#365314"); lime.insert("950", "#1a2e05");
633 colors.insert("lime", lime);
634
635 let mut green = IndexMap::new();
636 green.insert("50", "#f0fdf4"); green.insert("100", "#dcfce7"); green.insert("200", "#bbf7d0");
637 green.insert("300", "#86efac"); green.insert("400", "#4ade80"); green.insert("500", "#22c55e");
638 green.insert("600", "#16a34a"); green.insert("700", "#15803d"); green.insert("800", "#166534");
639 green.insert("900", "#14532d"); green.insert("950", "#052e16");
640 colors.insert("green", green);
641
642 let mut emerald = IndexMap::new();
643 emerald.insert("50", "#ecfdf5"); emerald.insert("100", "#d1fae5"); emerald.insert("200", "#a7f3d0");
644 emerald.insert("300", "#6ee7b7"); emerald.insert("400", "#34d399"); emerald.insert("500", "#10b981");
645 emerald.insert("600", "#059669"); emerald.insert("700", "#047857"); emerald.insert("800", "#065f46");
646 emerald.insert("900", "#064e3b"); emerald.insert("950", "#022c22");
647 colors.insert("emerald", emerald);
648
649 let mut teal = IndexMap::new();
650 teal.insert("50", "#f0fdfa"); teal.insert("100", "#ccfbf1"); teal.insert("200", "#99f6e4");
651 teal.insert("300", "#5eead4"); teal.insert("400", "#2dd4bf"); teal.insert("500", "#14b8a6");
652 teal.insert("600", "#0d9488"); teal.insert("700", "#0f766e"); teal.insert("800", "#115e59");
653 teal.insert("900", "#134e4a"); teal.insert("950", "#042f2e");
654 colors.insert("teal", teal);
655
656 let mut cyan = IndexMap::new();
657 cyan.insert("50", "#ecfeff"); cyan.insert("100", "#cffafe"); cyan.insert("200", "#a5f3fc");
658 cyan.insert("300", "#67e8f9"); cyan.insert("400", "#22d3ee"); cyan.insert("500", "#06b6d4");
659 cyan.insert("600", "#0891b2"); cyan.insert("700", "#0e7490"); cyan.insert("800", "#155e75");
660 cyan.insert("900", "#164e63"); cyan.insert("950", "#083344");
661 colors.insert("cyan", cyan);
662
663 let mut sky = IndexMap::new();
664 sky.insert("50", "#f0f9ff"); sky.insert("100", "#e0f2fe"); sky.insert("200", "#bae6fd");
665 sky.insert("300", "#7dd3fc"); sky.insert("400", "#38bdf8"); sky.insert("500", "#0ea5e9");
666 sky.insert("600", "#0284c7"); sky.insert("700", "#0369a1"); sky.insert("800", "#075985");
667 sky.insert("900", "#0c4a6e"); sky.insert("950", "#082f49");
668 colors.insert("sky", sky);
669
670 let mut blue = IndexMap::new();
671 blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
672 blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
673 blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
674 blue.insert("900", "#1e3a8a"); blue.insert("950", "#172554");
675 colors.insert("blue", blue);
676
677 let mut indigo = IndexMap::new();
678 indigo.insert("50", "#eef2ff"); indigo.insert("100", "#e0e7ff"); indigo.insert("200", "#c7d2fe");
679 indigo.insert("300", "#a5b4fc"); indigo.insert("400", "#818cf8"); indigo.insert("500", "#6366f1");
680 indigo.insert("600", "#4f46e5"); indigo.insert("700", "#4338ca"); indigo.insert("800", "#3730a3");
681 indigo.insert("900", "#312e81"); indigo.insert("950", "#1e1b4b");
682 colors.insert("indigo", indigo);
683
684 let mut violet = IndexMap::new();
685 violet.insert("50", "#f5f3ff"); violet.insert("100", "#ede9fe"); violet.insert("200", "#ddd6fe");
686 violet.insert("300", "#c4b5fd"); violet.insert("400", "#a78bfa"); violet.insert("500", "#8b5cf6");
687 violet.insert("600", "#7c3aed"); violet.insert("700", "#6d28d9"); violet.insert("800", "#5b21b6");
688 violet.insert("900", "#4c1d95"); violet.insert("950", "#2e1065");
689 colors.insert("violet", violet);
690
691 let mut purple = IndexMap::new();
692 purple.insert("50", "#faf5ff"); purple.insert("100", "#f3e8ff"); purple.insert("200", "#e9d5ff");
693 purple.insert("300", "#d8b4fe"); purple.insert("400", "#c084fc"); purple.insert("500", "#a855f7");
694 purple.insert("600", "#9333ea"); purple.insert("700", "#7e22ce"); purple.insert("800", "#6b21a8");
695 purple.insert("900", "#581c87"); purple.insert("950", "#3b0764");
696 colors.insert("purple", purple);
697
698 let mut fuchsia = IndexMap::new();
699 fuchsia.insert("50", "#fdf4ff"); fuchsia.insert("100", "#fae8ff"); fuchsia.insert("200", "#f5d0fe");
700 fuchsia.insert("300", "#f0abfc"); fuchsia.insert("400", "#e879f9"); fuchsia.insert("500", "#d946ef");
701 fuchsia.insert("600", "#c026d3"); fuchsia.insert("700", "#a21caf"); fuchsia.insert("800", "#86198f");
702 fuchsia.insert("900", "#701a75"); fuchsia.insert("950", "#4a044e");
703 colors.insert("fuchsia", fuchsia);
704
705 let mut pink = IndexMap::new();
706 pink.insert("50", "#fdf2f8"); pink.insert("100", "#fce7f3"); pink.insert("200", "#fbcfe8");
707 pink.insert("300", "#f9a8d4"); pink.insert("400", "#f472b6"); pink.insert("500", "#ec4899");
708 pink.insert("600", "#db2777"); pink.insert("700", "#be185d"); pink.insert("800", "#9d174d");
709 pink.insert("900", "#831843"); pink.insert("950", "#500724");
710 colors.insert("pink", pink);
711
712 let mut rose = IndexMap::new();
713 rose.insert("50", "#fff1f2"); rose.insert("100", "#ffe4e6"); rose.insert("200", "#fecdd3");
714 rose.insert("300", "#fda4af"); rose.insert("400", "#fb7185"); rose.insert("500", "#f43f5e");
715 rose.insert("600", "#e11d48"); rose.insert("700", "#be123c"); rose.insert("800", "#9f1239");
716 rose.insert("900", "#881337"); rose.insert("950", "#4c0519");
717 colors.insert("rose", rose);
718
719 colors
720});
721
722fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
723 let mut out = input.to_string();
724 for cap in RE_VAR.captures_iter(input) {
725 if let Some(name) = cap.get(1) {
726 let key = name.as_str();
727 if let Some(val) = vars.get(key) {
728 out = out.replace(&format!("var(--{})", key), val);
730 out = out.replace(&format!("var({})", key), val);
731 }
732 }
733 }
734 if out.starts_with('$') {
735 if let Some(val) = vars.get(&out[1..]) {
736 return val.clone();
737 }
738 }
739 out
740}
741
742fn camel_case(name: &str) -> String {
743 let mut out = String::new();
744 let mut upper = false;
745 for ch in name.chars() {
746 if ch == '-' {
747 upper = true;
748 continue;
749 }
750 if upper {
751 out.extend(ch.to_uppercase());
752 upper = false;
753 } else {
754 out.push(ch);
755 }
756 }
757 out
758}
759
760fn css_value_to_rn(
761 value: &serde_json::Value,
762 vars: &IndexMap<String, String>,
763) -> serde_json::Value {
764 match value {
765 serde_json::Value::String(s) => {
766 let s2 = resolve_vars(s, vars);
767 if let Some(n) = s2.strip_suffix("px") {
768 if let Ok(parsed) = n.trim().parse::<f64>() {
769 return json!(parsed);
770 }
771 }
772 json!(s2)
773 }
774 _ => value.clone(),
775 }
776}
777
778fn merge_rn_props(
779 into: &mut IndexMap<String, serde_json::Value>,
780 css_props: &CssProps,
781 vars: &IndexMap<String, String>,
782) {
783 for (k, v) in css_props.iter() {
784 let rn_key = match k.as_str() {
785 "background-color" => "backgroundColor".to_string(),
787 "text-align" => "textAlign".to_string(),
788 _ => camel_case(k),
789 };
790 let rn_val = css_value_to_rn(v, vars);
791 into.insert(rn_key, rn_val);
792 }
793}
794
795fn dynamic_css_properties_for_class(class: &str, vars: &IndexMap<String, String>) -> Option<CssProps> {
796 match class {
798 "block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("block")); return Some(p); }
799 "inline-block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-block")); return Some(p); }
800 "inline" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline")); return Some(p); }
801 "inline-flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-flex")); return Some(p); }
802 "grid" => { let mut p = CssProps::new(); p.insert("display".into(), json!("grid")); return Some(p); }
803 "hidden" => { let mut p = CssProps::new(); p.insert("display".into(), json!("none")); return Some(p); }
804 _ => {}
805 }
806 match class {
808 "flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); return Some(p); }
809 "flex-row" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("row")); return Some(p); }
810 "flex-col" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("column")); return Some(p); }
811 "flex-1" => { let mut p = CssProps::new(); p.insert("flex".into(), json!(1)); return Some(p); }
812 _ => {}
813 }
814 if let Some(rest) = class.strip_prefix("items-") {
815 let mut p = CssProps::new();
816 let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "stretch" => "stretch", other => other };
817 p.insert("align-items".into(), json!(v));
818 return Some(p);
819 }
820 if let Some(rest) = class.strip_prefix("justify-") {
821 let mut p = CssProps::new();
822 let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "between" => "space-between", "around" => "space-around", "evenly" => "space-evenly", other => other };
823 p.insert("justify-content".into(), json!(v));
824 return Some(p);
825 }
826 if let Some(value) = class.strip_prefix("p-") {
827 return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
828 }
829 if let Some(value) = class.strip_prefix("px-") {
830 return parse_tailwind_spacing(value, &|px| padding_props(&["padding-left", "padding-right"], px));
831 }
832 if let Some(value) = class.strip_prefix("py-") {
833 return parse_tailwind_spacing(value, &|px| padding_props(&["padding-top", "padding-bottom"], px));
834 }
835 for &(prefix, prop) in &[("pt-", "padding-top"), ("pr-", "padding-right"), ("pb-", "padding-bottom"), ("pl-", "padding-left")] {
836 if let Some(value) = class.strip_prefix(prefix) {
837 return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
838 }
839 }
840 if let Some(value) = class.strip_prefix("m-") {
842 return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
843 }
844 if let Some(value) = class.strip_prefix("mx-") {
845 return parse_tailwind_spacing(value, &|px| margin_props(&["margin-left", "margin-right"], px));
846 }
847 if let Some(value) = class.strip_prefix("my-") {
848 return parse_tailwind_spacing(value, &|px| margin_props(&["margin-top", "margin-bottom"], px));
849 }
850 for &(prefix, prop) in &[("mt-", "margin-top"), ("mr-", "margin-right"), ("mb-", "margin-bottom"), ("ml-", "margin-left")] {
851 if let Some(value) = class.strip_prefix(prefix) {
852 return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
853 }
854 }
855 if let Some(value) = class.strip_prefix("gap-") {
857 if !value.starts_with("x-") && !value.starts_with("y-") {
858 return parse_tailwind_spacing(value, &|px| {
859 let mut props = CssProps::new();
860 props.insert("gap".into(), json!(format!("{}px", px)));
861 props
862 });
863 }
864 }
865 if let Some(value) = class.strip_prefix("gap-x-") {
866 return parse_tailwind_spacing(value, &|px| {
867 let mut props = CssProps::new();
868 props.insert("column-gap".into(), json!(format!("{}px", px)));
869 props
870 });
871 }
872 if let Some(value) = class.strip_prefix("gap-y-") {
873 return parse_tailwind_spacing(value, &|px| {
874 let mut props = CssProps::new();
875 props.insert("row-gap".into(), json!(format!("{}px", px)));
876 props
877 });
878 }
879 if let Some(value) = class.strip_prefix("space-x-") {
881 return parse_tailwind_spacing(value, &|px| {
882 let mut props = CssProps::new();
883 props.insert("--space-x".into(), json!(format!("{}px", px)));
886 props
887 });
888 }
889 if let Some(value) = class.strip_prefix("space-y-") {
890 return parse_tailwind_spacing(value, &|px| {
891 let mut props = CssProps::new();
892 props.insert("--space-y".into(), json!(format!("{}px", px)));
893 props
894 });
895 }
896 match class {
898 "font-thin" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("100")); return Some(p); }
899 "font-extralight" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("200")); return Some(p); }
900 "font-light" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("300")); return Some(p); }
901 "font-normal" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("400")); return Some(p); }
902 "font-medium" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("500")); return Some(p); }
903 "font-semibold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("600")); return Some(p); }
904 "font-bold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("700")); return Some(p); }
905 "font-extrabold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("800")); return Some(p); }
906 "font-black" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("900")); return Some(p); }
907 _ => {}
908 }
909 match class {
911 "font-sans" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("system-ui, -apple-system, sans-serif")); return Some(p); }
912 "font-serif" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("Georgia, serif")); return Some(p); }
913 "font-mono" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("ui-monospace, monospace")); return Some(p); }
914 _ => {}
915 }
916 match class {
918 "text-xs" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("12px")); p.insert("line-height".into(), json!("16px")); return Some(p); }
919 "text-sm" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("14px")); p.insert("line-height".into(), json!("20px")); return Some(p); }
920 "text-base" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("16px")); p.insert("line-height".into(), json!("24px")); return Some(p); }
921 "text-lg" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("18px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
922 "text-xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("20px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
923 "text-2xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("24px")); p.insert("line-height".into(), json!("32px")); return Some(p); }
924 "text-3xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("30px")); p.insert("line-height".into(), json!("36px")); return Some(p); }
925 "text-4xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("36px")); p.insert("line-height".into(), json!("40px")); return Some(p); }
926 "text-5xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("48px")); p.insert("line-height".into(), json!("1")); return Some(p); }
927 "text-6xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("60px")); p.insert("line-height".into(), json!("1")); return Some(p); }
928 _ => {}
929 }
930 match class {
932 "text-left" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("left")); return Some(p); }
933 "text-center" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("center")); return Some(p); }
934 "text-right" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("right")); return Some(p); }
935 "text-justify" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("justify")); return Some(p); }
936 _ => {}
937 }
938 match class {
940 "overflow-auto" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("auto")); return Some(p); }
941 "overflow-hidden" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("hidden")); return Some(p); }
942 "overflow-visible" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("visible")); return Some(p); }
943 "overflow-scroll" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("scroll")); return Some(p); }
944 "overflow-x-auto" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("auto")); return Some(p); }
945 "overflow-x-hidden" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("hidden")); return Some(p); }
946 "overflow-x-scroll" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("scroll")); return Some(p); }
947 "overflow-y-auto" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("auto")); return Some(p); }
948 "overflow-y-hidden" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("hidden")); return Some(p); }
949 "overflow-y-scroll" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("scroll")); return Some(p); }
950 _ => {}
951 }
952 match class {
954 "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); }
955 "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); }
956 "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); }
957 "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); }
958 "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); }
959 "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); }
960 "shadow-none" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("none")); return Some(p); }
961 _ => {}
962 }
963 if let Some(arb_value) = parse_arbitrary_value(class) {
965 return Some(arb_value);
966 }
967 if let Some(rest) = class.strip_prefix("text-") {
969 if let Some(hex) = get_tailwind_color(rest) {
970 let mut props = CssProps::new();
971 props.insert("color".into(), json!(hex));
972 return Some(props);
973 }
974 }
975 if let Some(rest) = class.strip_prefix("bg-") {
977 if let Some(hex) = get_tailwind_color(rest) {
978 let mut props = CssProps::new();
979 props.insert("background-color".into(), json!(hex));
980 return Some(props);
981 }
982 }
983 if let Some(rest) = class.strip_prefix("divide-") {
985 if let Some(hex) = get_tailwind_color(rest) {
986 let mut props = CssProps::new();
987 props.insert("border-color".into(), json!(hex));
988 return Some(props);
989 }
990 }
991 if class == "border" {
992 return Some(border_props(None, 1, vars));
993 }
994 if let Some(rest) = class.strip_prefix("border-") {
995 let parts: Vec<&str> = rest.split('-').collect();
1003
1004 let valid_sides = ["t", "b", "l", "r", "x", "y"];
1006 let (side, color_or_width_parts) = if parts.len() > 1 && valid_sides.contains(&parts[0]) {
1007 (Some(parts[0]), &parts[1..])
1008 } else {
1009 (None, &parts[..])
1010 };
1011
1012 if color_or_width_parts.len() == 2 {
1014 let color_shade = color_or_width_parts.join("-");
1016 if let Some(hex) = get_tailwind_color(&color_shade) {
1017 let mut props = CssProps::new();
1018 let prop_name = if let Some(s) = side {
1019 format!("border-{}-color", s)
1020 } else {
1021 "border-color".to_string()
1022 };
1023 props.insert(prop_name, json!(hex));
1024 return Some(props);
1025 }
1026 }
1027
1028 if color_or_width_parts.len() == 1 {
1030 let potential_color = format!("{}-500", color_or_width_parts[0]);
1031 if let Some(hex) = get_tailwind_color(&potential_color) {
1032 let mut props = CssProps::new();
1033 let prop_name = if let Some(s) = side {
1034 format!("border-{}-color", s)
1035 } else {
1036 "border-color".to_string()
1037 };
1038 props.insert(prop_name, json!(hex));
1039 return Some(props);
1040 }
1041 }
1042
1043 if color_or_width_parts.len() == 1 {
1045 if let Ok(width) = color_or_width_parts[0].parse::<i32>() {
1046 return Some(border_props(side, width, vars));
1047 }
1048 }
1049 }
1050 if class == "rounded" { return Some(rounded_props(None, Some("md"))); }
1052 if let Some(sz) = class.strip_prefix("rounded-") {
1053 return Some(rounded_props(None, Some(sz)));
1054 }
1055 for &(pref, side) in &[("rounded-t", "t"), ("rounded-b", "b"), ("rounded-l", "l"), ("rounded-r", "r")] {
1056 if class == pref { return Some(rounded_props(Some(side), Some("md"))); }
1057 if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
1058 return Some(rounded_props(Some(side), Some(sz)));
1059 }
1060 }
1061 if let Some(cur) = class.strip_prefix("cursor-") {
1063 let mut props = CssProps::new();
1064 props.insert("cursor".into(), json!(match cur {
1065 "pointer" => "pointer",
1066 "default" => "default",
1067 "text" => "text",
1068 "move" => "move",
1069 "wait" => "wait",
1070 "not-allowed" => "not-allowed",
1071 other => other,
1072 }));
1073 return Some(props);
1074 }
1075 if class == "transition" || class == "transition-all" {
1077 let mut props = CssProps::new();
1078 props.insert("transition-property".into(), json!("all"));
1079 props.insert("transition-duration".into(), json!("150ms"));
1080 props.insert("transition-timing-function".into(), json!("ease-in-out"));
1081 return Some(props);
1082 }
1083 if class == "transition-none" {
1084 let mut props = CssProps::new();
1085 props.insert("transition-property".into(), json!("none"));
1086 props.insert("transition-duration".into(), json!("0ms"));
1087 return Some(props);
1088 }
1089 if let Some(rest) = class.strip_prefix("transition-") {
1090 let mut props = CssProps::new();
1092 let property = match rest {
1093 "colors" => "color, background-color, border-color, fill, stroke",
1094 "opacity" => "opacity",
1095 "transform" => "transform",
1096 "shadow" => "box-shadow",
1097 other => other,
1098 };
1099 props.insert("transition-property".into(), json!(property));
1100 props.insert("transition-duration".into(), json!("150ms"));
1101 props.insert("transition-timing-function".into(), json!("ease-in-out"));
1102 return Some(props);
1103 }
1104 if let Some(val) = class.strip_prefix("w-") {
1106 return width_like_props("width", val);
1107 }
1108 if let Some(val) = class.strip_prefix("min-w-") {
1109 return width_like_props("min-width", val);
1110 }
1111 if let Some(val) = class.strip_prefix("max-w-") {
1112 return width_like_props("max-width", val);
1113 }
1114 if let Some(val) = class.strip_prefix("h-") {
1116 return width_like_props("height", val);
1117 }
1118 if let Some(val) = class.strip_prefix("min-h-") {
1119 return width_like_props("min-height", val);
1120 }
1121 if let Some(val) = class.strip_prefix("max-h-") {
1122 return width_like_props("max-height", val);
1123 }
1124 None
1125}
1126
1127fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
1128where
1129 F: Fn(i32) -> CssProps,
1130{
1131 if let Ok(n) = value.parse::<i32>() {
1132 let px = n * 4;
1133 return Some(builder(px));
1134 }
1135 None
1136}
1137
1138fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
1139 let mut props = CssProps::new();
1140 let val = format!("{}px", px_value);
1141 for key in keys {
1142 props.insert((*key).into(), json!(&val));
1143 }
1144 props
1145}
1146
1147fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
1148 let mut props = CssProps::new();
1149 let val = format!("{}px", px_value);
1150 for key in keys {
1151 props.insert((*key).into(), json!(&val));
1152 }
1153 props
1154}
1155
1156fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
1157 let mut props = CssProps::new();
1158 let width_str = format!("{}px", width);
1159 match side {
1160 None => {
1161 props.insert("border-width".into(), json!(&width_str));
1162 }
1163 Some("t") => {
1164 props.insert("border-top-width".into(), json!(&width_str));
1165 }
1166 Some("b") => {
1167 props.insert("border-bottom-width".into(), json!(&width_str));
1168 }
1169 Some("l") => {
1170 props.insert("border-left-width".into(), json!(&width_str));
1171 }
1172 Some("r") => {
1173 props.insert("border-right-width".into(), json!(&width_str));
1174 }
1175 Some("x") => {
1176 props.insert("border-left-width".into(), json!(&width_str));
1177 props.insert("border-right-width".into(), json!(&width_str));
1178 }
1179 Some("y") => {
1180 props.insert("border-top-width".into(), json!(&width_str));
1181 props.insert("border-bottom-width".into(), json!(&width_str));
1182 }
1183 _ => {
1184 props.insert("border-width".into(), json!(&width_str));
1185 }
1186 };
1187 props.insert("border-color".into(), json!("var(border)"));
1188 props.insert("border-style".into(), json!("solid"));
1189 props
1190}
1191
1192fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
1193 let mut props = CssProps::new();
1194 let px = match size.unwrap_or("md") {
1195 "none" => 0,
1196 "sm" => 2,
1197 "md" => 4,
1198 "lg" => 8,
1199 "xl" => 12,
1200 "2xl" => 16,
1201 "3xl" => 24,
1202 "full" => 9999,
1203 s => s.parse::<i32>().unwrap_or(4),
1204 };
1205 let v = json!(format!("{}px", px));
1206 match side {
1207 None => { props.insert("border-radius".into(), v); }
1208 Some("t") => {
1209 props.insert("border-top-left-radius".into(), v.clone());
1210 props.insert("border-top-right-radius".into(), v);
1211 }
1212 Some("b") => {
1213 props.insert("border-bottom-left-radius".into(), v.clone());
1214 props.insert("border-bottom-right-radius".into(), v);
1215 }
1216 Some("l") => { props.insert("border-top-left-radius".into(), v.clone()); props.insert("border-bottom-left-radius".into(), v); }
1217 Some("r") => { props.insert("border-top-right-radius".into(), v.clone()); props.insert("border-bottom-right-radius".into(), v); }
1218 _ => { props.insert("border-radius".into(), v); }
1219 }
1220 props
1221}
1222
1223fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
1224 let mut props = CssProps::new();
1225 let value = match token {
1226 "full" => Some("100%".to_string()),
1227 "screen" => Some(if prop == "width" { "100vw" } else { "100vh" }.to_string()),
1228 "min" => Some("min-content".to_string()),
1229 "max" => Some("max-content".to_string()),
1230 "fit" => Some("fit-content".to_string()),
1231 "auto" => Some("auto".to_string()),
1232 "px" => Some("1px".to_string()),
1233 other => {
1234 if let Some((a, b)) = other.split_once('/') {
1236 if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
1237 let pct = (na / nb) * 100.0;
1238 Some(format!("{}%", trim_trailing_zeros(pct)))
1239 } else { None }
1240 } else if let Ok(n) = other.parse::<i32>() {
1241 Some(format!("{}px", n * 4))
1242 } else {
1243 None
1244 }
1245 }
1246 }?;
1247 props.insert(prop.into(), json!(value));
1248 Some(props)
1249}
1250
1251fn trim_trailing_zeros(num: f64) -> String {
1252 let mut s = format!("{:.6}", num);
1253 while s.contains('.') && s.ends_with('0') { s.pop(); }
1254 if s.ends_with('.') { s.pop(); }
1255 s
1256}
1257
1258fn css_escape_class(class: &str) -> String { class.replace(':', "\\:") }
1263
1264fn class_to_selector(class: &str) -> String {
1265 let (_bp, hover, base) = parse_prefixed_class(class);
1266 if hover {
1267 format!(".{}:hover", css_escape_class(&base))
1268 } else {
1269 format!(".{}", css_escape_class(&base))
1270 }
1271}
1272
1273pub fn post_process_css(
1280 raw_rules: &[(String, CssProps)],
1281 vars: &IndexMap<String, String>,
1282) -> String {
1283 let mut normal = vec![];
1285 let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
1286 for (sel, props) in raw_rules.iter() {
1287 if let Some((media, inner)) = sel.split_once('{') {
1288 if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
1289 let inner_sel = inner.trim_end_matches('}').to_string();
1290 media_map
1291 .entry(media.trim().to_string())
1292 .or_default()
1293 .push((inner_sel, props.clone()));
1294 continue;
1295 }
1296 }
1297 normal.push((sel.clone(), props.clone()));
1298 }
1299 let mut out = String::new();
1300 for (sel, props) in normal {
1301 out.push_str(&sel);
1302 out.push('{');
1303 out.push_str(&css_props_string(&props, vars));
1304 out.push_str("}\n");
1305 }
1306 for (media, entries) in media_map {
1307 out.push_str(&media);
1308 out.push('{');
1309 for (sel, props) in entries {
1310 out.push_str(&sel);
1311 out.push('{');
1312 out.push_str(&css_props_string(&props, vars));
1313 out.push_str("}");
1314 }
1315 out.push_str("}\n");
1316 }
1317 out
1318}
1319
1320fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1323 let parts: Vec<&str> = class.split(':').collect();
1325 if parts.len() == 1 {
1326 return (None, false, class.to_string());
1327 }
1328 let mut bp: Option<String> = None;
1329 let mut hover = false;
1330 for &p in &parts[..parts.len() - 1] {
1331 match p {
1332 "hover" => hover = true,
1333 "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
1334 _ => {}
1335 }
1336 }
1337 let base = parts.last().unwrap().to_string();
1338 (bp, hover, base)
1339}
1340
1341fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
1342 if let Some(k) = bp_key {
1343 if let Some(val) = bps.get(k) {
1344 return format!("@media (min-width: {}) {{{}}}", val, selector);
1345 }
1346 }
1347 selector.to_string()
1348}
1349
1350fn get_tailwind_color(color_shade: &str) -> Option<String> {
1352 let parts: Vec<&str> = color_shade.split('-').collect();
1353 if parts.len() != 2 {
1354 return None;
1355 }
1356 let color_name = parts[0];
1357 let shade = parts[1];
1358
1359 TAILWIND_COLORS
1360 .get(color_name)
1361 .and_then(|shades| shades.get(shade))
1362 .map(|&hex| hex.to_string())
1363}
1364
1365fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
1367 if let Some(bracket_start) = class.find('[') {
1369 if !class.ends_with(']') {
1370 return None;
1371 }
1372 let prefix = &class[..bracket_start];
1373 let value = &class[bracket_start + 1..class.len() - 1];
1374
1375 let mut props = CssProps::new();
1376 match prefix {
1377 "bg" => {
1378 props.insert("background-color".into(), json!(value));
1379 return Some(props);
1380 }
1381 "text" => {
1382 props.insert("color".into(), json!(value));
1383 return Some(props);
1384 }
1385 "border" => {
1386 props.insert("border-color".into(), json!(value));
1387 return Some(props);
1388 }
1389 "divide" => {
1390 props.insert("border-color".into(), json!(value));
1391 return Some(props);
1392 }
1393 _ => return None,
1394 }
1395 }
1396 None
1397}
1398
1399pub mod api {
1401 pub use super::{SelectorStyles, State};
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use super::*;
1407
1408 #[test]
1409 fn default_theme_has_p2() {
1410 let mut st = State::new_default();
1411 let css = st.css_for_web();
1412 assert!(css.contains(".p-2{"));
1413 assert!(css.contains("padding:8px"));
1414 }
1415
1416 #[test]
1417 fn rn_conversion() {
1418 let st = State::new_default();
1419 let out = st.rn_styles_for("button", &[]);
1420 assert!(out.get("backgroundColor").is_some());
1421 }
1422
1423 #[test]
1424 fn embedded_defaults_and_version() {
1425 let st = State::default_state();
1427 assert!(st.themes.contains_key("default"));
1428 let def = st.themes.get("default").unwrap();
1429 assert!(def.variables.contains_key("primary"));
1430
1431 let v = get_version();
1433 assert!(!v.is_empty());
1434 }
1435
1436 #[test]
1437 fn border_color_with_direction() {
1438 let mut st = State::new_default();
1439
1440 st.register_tailwind_classes(["border-b-blue-500".to_string()]);
1442 let css = st.css_for_web();
1443 assert!(css.contains(".border-b-blue-500{"));
1444 assert!(css.contains("border-bottom-color:#3b82f6") || css.contains("border-b-color:#3b82f6"));
1445
1446 st.register_tailwind_classes(["border-t-red-500".to_string()]);
1448 let css = st.css_for_web();
1449 assert!(css.contains(".border-t-red-500{"));
1450
1451 st.register_tailwind_classes(["border-blue-500".to_string()]);
1453 let css = st.css_for_web();
1454 assert!(css.contains(".border-blue-500{"));
1455 assert!(css.contains("border-color:#3b82f6"));
1456 }
1457
1458 #[test]
1459 fn border_width_with_direction() {
1460 let mut st = State::new_default();
1461
1462 st.register_tailwind_classes(["border-b-2".to_string()]);
1464 let css = st.css_for_web();
1465 assert!(css.contains(".border-b-2{"));
1466 assert!(css.contains("border-bottom-width:2px"));
1467
1468 st.register_tailwind_classes(["border-2".to_string()]);
1470 let css = st.css_for_web();
1471 assert!(css.contains(".border-2{"));
1472 assert!(css.contains("border-width:2px"));
1473 }
1474
1475 #[test]
1476 fn display_flex_hover_breakpoint() {
1477 let mut st = State::new_default();
1478 st.register_tailwind_classes([
1479 "block".into(),
1480 "inline-flex".into(),
1481 "hidden".into(),
1482 "md:flex".into(),
1483 "md:hover:block".into(),
1484 ]);
1485 let css = st.css_for_web();
1486 assert!(css.contains(".block{"));
1487 assert!(css.contains("display:block"));
1488 assert!(css.contains(".inline-flex{"));
1489 assert!(css.contains("display:inline-flex"));
1490 assert!(css.contains(".hidden{"));
1491 assert!(css.contains("display:none"));
1492 assert!(css.contains("@media (min-width: 768px)"));
1494 assert!(css.contains(".flex{display:flex"));
1495 assert!(css.contains(":hover{display:block"));
1497
1498 let rn = st.rn_styles_for("div", &["md:flex".into()]);
1500 assert_eq!(rn.get("display").and_then(|v| v.as_str()), Some("flex"));
1501 }
1502}
1503
1504#[cfg(all(target_os = "android", feature = "android"))]
1505mod android_jni;
1506
1507#[cfg(target_vendor = "apple")]
1508mod ios_ffi;