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>; #[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 pub themes: IndexMap<String, ThemeEntry>,
29 pub default_theme: String,
30 pub current_theme: String,
31 #[serde(default)]
33 pub theme_variables: IndexMap<String, IndexMap<String, String>>, #[serde(default)]
35 pub variables: IndexMap<String, String>, #[serde(default)]
37 pub breakpoints: IndexMap<String, String>, #[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 return bundled_state();
54 }
55
56 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 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 "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 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 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 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 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 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 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 fn theme_chain(&self) -> Vec<String> {
202 let mut chain = Vec::new();
203 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 let mut seen: IndexSet<String> = IndexSet::new();
212 while !seen.contains(¤t_name) {
213 seen.insert(current_name.clone());
214 chain.push(current_name.clone());
215 let inherits = self.themes.get(¤t_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 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 for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
235 let chain = self.theme_chain();
237 for name in chain.into_iter() {
238 if let Some(entry) = self.themes.get(&name) {
239 for (sel, props) in entry.selectors.iter() {
241 let e = selectors.entry(sel.clone()).or_default();
242 merge_props(e, props);
243 }
244 for (k, v) in entry.variables.iter() {
246 vars.insert(k.clone(), v.clone());
247 }
248 }
249 }
250 (selectors, vars)
251 }
252
253 pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
255 let mut bps: IndexMap<String, String> = IndexMap::new();
256 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]
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#[wasm_bindgen]
290pub fn get_version() -> String {
291 env!("CARGO_PKG_VERSION").to_string()
293}
294
295pub fn version() -> &'static str {
297 env!("CARGO_PKG_VERSION")
298}
299
300#[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
316fn 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
337static RE_VAR: Lazy<Regex> = Lazy::new(|| Regex::new(r"var\(\s*-{0,2}([a-zA-Z0-9_-]+)\s*\)").unwrap());
339
340static 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 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 "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 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 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 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 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 if let Some(value) = class.strip_prefix("space-x-") {
660 return parse_tailwind_spacing(value, &|px| {
661 let mut props = CssProps::new();
662 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 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 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 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 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 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 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 if let Some(arb_value) = parse_arbitrary_value(class) {
744 return Some(arb_value);
745 }
746 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 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 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 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 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 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 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 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 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 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 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 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
999fn 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
1014pub fn post_process_css(
1021 raw_rules: &[(String, CssProps)],
1022 vars: &IndexMap<String, String>,
1023) -> String {
1024 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
1061fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1064 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
1091fn 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
1106fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
1108 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
1140pub 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 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 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 assert!(css.contains("@media (min-width: 768px)"));
1196 assert!(css.contains(".flex{display:flex"));
1197 assert!(css.contains(":hover{display:block"));
1199
1200 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;