1use indexmap::{IndexMap, IndexSet};
2use once_cell::sync::Lazy;
3use serde::{de::Deserializer, Deserialize, Serialize};
4use serde_json::json;
5#[cfg(target_arch = "wasm32")]
6use wasm_bindgen::prelude::*;
7mod default_state;
8use default_state::bundled_state;
9
10fn default_display_density() -> f32 { 1.0 }
12fn default_scaled_density() -> f32 { 1.0 }
13
14pub type CssProps = IndexMap<String, serde_json::Value>;
15pub type SelectorStyles = IndexMap<String, CssProps>; fn dp_to_px(dp: f32, density: f32) -> i32 {
19 (dp * density).round() as i32
20}
21
22fn sp_to_px(sp: f32, scaled_density: f32) -> f32 {
24 sp * scaled_density
25}
26
27fn parse_and_convert_to_px(value: &serde_json::Value, density: f32) -> Option<serde_json::Value> {
29 match value {
30 serde_json::Value::Number(n) => {
31 let dp = n.as_f64()? as f32;
33 Some(serde_json::json!(dp_to_px(dp, density)))
34 }
35 serde_json::Value::String(s) => {
36 let trimmed = s.trim();
38 if trimmed.ends_with("px") {
39 let px = trimmed.trim_end_matches("px").trim().parse::<f32>().ok()?;
41 Some(serde_json::json!(dp_to_px(px, density)))
42 } else if trimmed.ends_with("dp") {
43 let dp = trimmed.trim_end_matches("dp").trim().parse::<f32>().ok()?;
44 Some(serde_json::json!(dp_to_px(dp, density)))
45 } else if let Ok(num) = trimmed.parse::<f32>() {
46 Some(serde_json::json!(dp_to_px(num, density)))
48 } else {
49 None
51 }
52 }
53 _ => None
54 }
55}
56
57fn deserialize_variables<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
58where
59 D: Deserializer<'de>,
60{
61 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
62 let mut out: IndexMap<String, String> = IndexMap::new();
63 if let Some(v) = value {
64 flatten_variables(None, &v, &mut out);
65 }
66 Ok(out)
67}
68
69fn flatten_variables(prefix: Option<&str>, value: &serde_json::Value, out: &mut IndexMap<String, String>) {
70 match value {
71 serde_json::Value::Object(map) => {
72 for (k, v) in map {
73 let key = if let Some(p) = prefix {
74 format!("{}.{}", p, k)
75 } else {
76 k.to_string()
77 };
78 flatten_variables(Some(&key), v, out);
79 }
80 }
81 serde_json::Value::Array(arr) => {
82 for (idx, v) in arr.iter().enumerate() {
83 let key = if let Some(p) = prefix {
84 format!("{}.{}", p, idx)
85 } else {
86 idx.to_string()
87 };
88 flatten_variables(Some(&key), v, out);
89 }
90 }
91 serde_json::Value::Null => {}
92 serde_json::Value::Bool(b) => {
93 if let Some(p) = prefix {
94 out.insert(p.to_string(), b.to_string());
95 }
96 }
97 serde_json::Value::Number(n) => {
98 if let Some(p) = prefix {
99 out.insert(p.to_string(), n.to_string());
100 }
101 }
102 serde_json::Value::String(s) => {
103 if let Some(p) = prefix {
104 out.insert(p.to_string(), s.clone());
105 }
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Default)]
111pub struct ThemeEntry {
112 #[serde(default)]
113 pub name: Option<String>,
114 #[serde(default)]
115 pub inherits: Option<String>,
116 #[serde(default)]
117 pub selectors: SelectorStyles,
118 #[serde(default, deserialize_with = "deserialize_variables")]
119 pub variables: IndexMap<String, String>,
120 #[serde(default, deserialize_with = "deserialize_variables")]
121 pub breakpoints: IndexMap<String, String>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
125pub struct State {
126 pub themes: IndexMap<String, ThemeEntry>,
128 pub default_theme: String,
129 pub current_theme: String,
130 #[serde(default = "default_display_density")]
132 pub display_density: f32, #[serde(default = "default_scaled_density")]
134 pub scaled_density: f32, #[serde(default)]
137 pub theme_variables: IndexMap<String, IndexMap<String, String>>, #[serde(default, deserialize_with = "deserialize_variables")]
139 pub variables: IndexMap<String, String>, #[serde(default, deserialize_with = "deserialize_variables")]
141 pub breakpoints: IndexMap<String, String>, #[serde(default)]
143 pub used_selectors: IndexSet<String>, #[serde(default)]
145 pub used_classes: IndexSet<String>, #[serde(default)]
147 pub used_tags: IndexSet<String>, #[serde(default)]
150 pub used_tag_classes: IndexSet<String>,
151}
152
153#[derive(thiserror::Error, Debug)]
154pub enum Error {
155 #[error("theme not found: {0}")]
156 ThemeNotFound(String),
157}
158
159impl State {
160 pub fn new_default() -> Self {
161 return bundled_state();
163 }
164
165 pub fn default_state() -> Self {
167 bundled_state()
168 }
169
170 pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
171 let name = theme.into();
172 if !self.themes.contains_key(&name) {
173 return Err(Error::ThemeNotFound(name));
174 }
175 self.current_theme = name;
176 Ok(())
177 }
178
179 pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
180 let name = name.into();
181 let entry = self.themes.entry(name).or_default();
182 for (sel, props) in styles.into_iter() {
183 let e = entry.selectors.entry(sel).or_default();
184 merge_props(e, &props);
185 }
186 }
187
188 pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
189 let cur = self.current_theme.clone();
191 let entry = self.themes.entry(cur).or_default();
192 entry.variables = vars;
193 }
194
195 pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
196 let cur = self.current_theme.clone();
197 let entry = self.themes.entry(cur).or_default();
198 entry.breakpoints = map;
199 }
200
201 pub fn process_styles(&self, mut styles: IndexMap<String, serde_json::Value>) -> IndexMap<String, serde_json::Value> {
202 let density = self.display_density;
203
204 if let Some(ph) = styles.get("paddingHorizontal").cloned() {
208 styles.entry("paddingLeft".into()).or_insert(ph.clone());
209 styles.entry("paddingRight".into()).or_insert(ph.clone());
210 }
211 if let Some(pv) = styles.get("paddingVertical").cloned() {
212 styles.entry("paddingTop".into()).or_insert(pv.clone());
213 styles.entry("paddingBottom".into()).or_insert(pv.clone());
214 }
215 if let Some(p) = styles.get("padding").cloned() {
216 styles.entry("paddingTop".into()).or_insert(p.clone());
217 styles.entry("paddingBottom".into()).or_insert(p.clone());
218 styles.entry("paddingLeft".into()).or_insert(p.clone());
219 styles.entry("paddingRight".into()).or_insert(p.clone());
220 }
221 if let Some(mh) = styles.get("marginHorizontal").cloned() {
222 styles.entry("marginLeft".into()).or_insert(mh.clone());
223 styles.entry("marginRight".into()).or_insert(mh.clone());
224 }
225 if let Some(mv) = styles.get("marginVertical").cloned() {
226 styles.entry("marginTop".into()).or_insert(mv.clone());
227 styles.entry("marginBottom".into()).or_insert(mv.clone());
228 }
229 if let Some(m) = styles.get("margin").cloned() {
230 styles.entry("marginTop".into()).or_insert(m.clone());
231 styles.entry("marginBottom".into()).or_insert(m.clone());
232 styles.entry("marginLeft".into()).or_insert(m.clone());
233 styles.entry("marginRight".into()).or_insert(m.clone());
234 }
235 if let Some(r) = styles.get("borderRadius").cloned() {
236 styles.entry("borderTopLeftRadius".into()).or_insert(r.clone());
237 styles.entry("borderTopRightRadius".into()).or_insert(r.clone());
238 styles.entry("borderBottomLeftRadius".into()).or_insert(r.clone());
239 styles.entry("borderBottomRightRadius".into()).or_insert(r.clone());
240 }
241
242 let dimension_props = [
244 "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
245 "padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
246 "paddingHorizontal", "paddingVertical",
247 "margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
248 "marginHorizontal", "marginVertical",
249 "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius",
250 "borderWidth", "borderTopWidth", "borderBottomWidth", "borderLeftWidth", "borderRightWidth",
251 "gap", "rowGap", "columnGap", "elevation", "fontSize", "lineHeight", "letterSpacing"
252 ];
253
254 for prop in &dimension_props {
255 if let Some(value) = styles.get(*prop).cloned() {
256 if let Some(converted) = parse_and_convert_to_px(&value, density) {
257 styles.insert(prop.to_string(), converted);
258 }
259 }
260 }
261
262 styles
263 }
264
265 pub fn set_default_theme(&mut self, name: impl Into<String>) {
266 self.default_theme = name.into();
267 }
268
269 pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, selectors: I) {
270 for s in selectors {
271 self.used_selectors.insert(s);
272 }
273 }
274
275 pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
276 for c in classes {
277 self.used_classes.insert(c);
278 }
279 }
280
281 pub fn register_tags<I: IntoIterator<Item = String>>(&mut self, tags: I) {
282 for t in tags {
283 self.used_tags.insert(t);
284 }
285 }
286
287 pub fn register_tag_class(&mut self, tag: impl Into<String>, class_: impl Into<String>) {
288 let key = format!("{}|{}", tag.into(), class_.into());
289 self.used_tag_classes.insert(key);
290 }
291
292
293 pub fn clear_usage(&mut self) {
294 self.used_selectors.clear();
295 self.used_classes.clear();
296 self.used_tags.clear();
297 self.used_tag_classes.clear();
298 }
299
300 pub fn to_json(&self) -> serde_json::Value {
301 json!({
302 "themes": self.themes,
303 "default_theme": self.default_theme,
304 "current_theme": self.current_theme,
305 "theme_variables": self.theme_variables,
307 "variables": self.variables,
308 "breakpoints": self.breakpoints,
309 "used_selectors": self.used_selectors,
310 "used_classes": self.used_classes,
311 "used_tags": self.used_tags,
312 "used_tag_classes": self.used_tag_classes,
313 })
314 }
315
316 pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
317 let state: State = serde_json::from_value(value)?;
318 Ok(state)
319 }
320
321 pub fn css_for_web(&self) -> String {
322 let (eff, vars) = self.effective_theme_all();
324 let bps = self.effective_breakpoints();
325 let mut rules: Vec<(String, CssProps)> = Vec::new();
326
327 let mut used_tags: IndexSet<String> = self.used_tags.clone();
329 let mut used_classes: IndexSet<String> = self.used_classes.clone();
330 for key in &self.used_tag_classes {
331 if let Some((t, c)) = split_tag_class_key(key) {
332 used_tags.insert(t);
333 used_classes.insert(c);
334 }
335 }
336
337 for (sel, props) in eff.iter() {
343 if should_emit_selector(sel, &used_tags, &used_classes, &self.used_tag_classes) {
344 rules.push((sel.clone(), props.clone()));
345 }
346 }
347
348 for class in &used_classes {
350 let (bp_key, hover, base) = parse_prefixed_class(class);
351 let selector = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
352
353 if let Some(props) = eff.get(&selector) {
355 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
356 rules.push((final_sel, props.clone()));
357 continue;
358 }
359 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
361 let sel = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
362 let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
363 rules.push((final_sel, dynamic_props));
364 continue;
365 }
366 if let Some(props) = eff.get(&base) {
368 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
369 rules.push((final_sel, props.clone()));
370 }
371 }
372
373 post_process_css(&rules, &vars)
374 }
375
376 pub fn android_base_styles(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
377 let (eff, vars) = self.effective_theme_all();
378 let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
379
380 out.insert("androidOrientation".to_string(), serde_json::json!("vertical"));
382
383 let mut defaults = CssProps::new();
385 match selector.to_lowercase().as_str() {
386 "div" => {
387 defaults.insert("width".into(), json!("match_parent"));
388 }
389 "p" => {
390 defaults.insert("width".into(), json!("match_parent"));
391 defaults.insert("margin-vertical".into(), json!("16px"));
392 }
393 "h1" => {
394 defaults.insert("width".into(), json!("match_parent"));
395 defaults.insert("font-size".into(), json!("32px"));
396 defaults.insert("font-weight".into(), json!("bold"));
397 defaults.insert("margin-vertical".into(), json!("21.44px"));
398 }
399 "h2" => {
400 defaults.insert("width".into(), json!("match_parent"));
401 defaults.insert("font-size".into(), json!("24px"));
402 defaults.insert("font-weight".into(), json!("bold"));
403 defaults.insert("margin-vertical".into(), json!("19.92px"));
404 }
405 "h3" => {
406 defaults.insert("width".into(), json!("match_parent"));
407 defaults.insert("font-size".into(), json!("18.72px"));
408 defaults.insert("font-weight".into(), json!("bold"));
409 defaults.insert("margin-vertical".into(), json!("18.72px"));
410 }
411 "h4" => {
412 defaults.insert("width".into(), json!("match_parent"));
413 defaults.insert("font-size".into(), json!("16px"));
414 defaults.insert("font-weight".into(), json!("bold"));
415 defaults.insert("margin-vertical".into(), json!("21.28px"));
416 }
417 "h5" => {
418 defaults.insert("width".into(), json!("match_parent"));
419 defaults.insert("font-size".into(), json!("13.28px"));
420 defaults.insert("font-weight".into(), json!("bold"));
421 defaults.insert("margin-vertical".into(), json!("22.17px"));
422 }
423 "h6" => {
424 defaults.insert("width".into(), json!("match_parent"));
425 defaults.insert("font-size".into(), json!("10.72px"));
426 defaults.insert("font-weight".into(), json!("bold"));
427 defaults.insert("margin-vertical".into(), json!("24.96px"));
428 }
429 "input" => {
430 defaults.insert("padding-vertical".into(), json!("8px"));
431 defaults.insert("padding-horizontal".into(), json!("12px"));
432 defaults.insert("border-radius".into(), json!("4px"));
433 defaults.insert("border-width".into(), json!("1px"));
434 defaults.insert("border-color".into(), json!("#cccccc"));
435 defaults.insert("background-color".into(), json!("#ffffff"));
436 defaults.insert("min-height".into(), json!("40px"));
437 defaults.insert("android-gravity".into(), json!("center_vertical"));
438 }
439 "button" => {
440 defaults.insert("padding-vertical".into(), json!("8px"));
441 defaults.insert("padding-horizontal".into(), json!("16px"));
442 defaults.insert("border-radius".into(), json!("4px"));
443 defaults.insert("background-color".into(), json!("#2196F3"));
444 defaults.insert("color".into(), json!("#ffffff"));
445 defaults.insert("android-gravity".into(), json!("center"));
446 }
447 _ => {}
448 }
449 merge_android_props(&mut out, &defaults, &vars);
450
451 if selector == "button" || classes.iter().any(|c| c.contains("bg-")) {
452 log::debug!("[android_base_styles] selector={} classes={:?}", selector, classes);
453 }
454
455 if let Some(props) = eff.get(selector) {
457 merge_android_props(&mut out, props, &vars);
458 }
459
460 for class in classes {
462 let normalized_class = if class.starts_with('.') {
464 class[1..].to_string()
465 } else {
466 class.clone()
467 };
468
469 let (_bp, _hover, base) = parse_prefixed_class(&normalized_class);
470 let sel = class_to_selector(&base);
472 if let Some(props) = eff.get(&sel) {
473 merge_android_props(&mut out, props, &vars);
474 continue;
475 }
476 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
478 merge_android_props(&mut out, &dynamic_props, &vars);
479 continue;
480 }
481 if let Some(props) = eff.get(&base) {
482 merge_android_props(&mut out, props, &vars);
483 }
484 }
485
486 if let Some(display) = out.get("display") {
488 if display.as_str() == Some("flex") && !out.contains_key("flexDirection") {
489 out.insert("flexDirection".to_string(), serde_json::json!("row"));
490 out.insert("androidOrientation".to_string(), serde_json::json!("horizontal"));
491 }
492 }
493
494 if !out.contains_key("flexDirection") {
496 match selector.to_lowercase().as_str() {
497 "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" => {
498 out.insert("flexDirection".to_string(), serde_json::json!("column"));
499 out.insert("androidOrientation".to_string(), serde_json::json!("vertical"));
500 }
501 _ => {}
502 }
503 }
504
505 if let Some(fd) = out.get("flexDirection").and_then(|v| v.as_str()) {
507 if !out.contains_key("androidOrientation") {
508 let orientation = if fd == "column" || fd == "column-reverse" {
509 "vertical"
510 } else {
511 "horizontal"
512 };
513 out.insert("androidOrientation".to_string(), serde_json::json!(orientation));
514 }
515 }
516
517 out
518 }
519
520 pub fn android_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
524 let mut styles = self.android_base_styles(selector, classes);
525
526 let density = self.display_density;
527 let scaled_density = self.scaled_density;
528
529 if let Some(flex_dir) = styles.get("flexDirection") {
531 let orientation = if flex_dir.as_str() == Some("row") { "horizontal" } else { "vertical" };
532 styles.shift_insert(0, "androidOrientation".to_string(), serde_json::json!(orientation));
533 }
534
535 let dimension_props = [
537 "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
538 "padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
539 "paddingHorizontal", "paddingVertical",
540 "margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
541 "marginHorizontal", "marginVertical",
542 "borderRadius", "borderWidth", "borderTopWidth", "borderBottomWidth",
543 "borderLeftWidth", "borderRightWidth",
544 "gap", "rowGap", "columnGap", "elevation", "lineHeight", "letterSpacing"
545 ];
546
547 for prop in &dimension_props {
548 if let Some(value) = styles.get(*prop).cloned() {
549 if let Some(converted) = parse_and_convert_to_px(&value, density) {
550 styles.insert(prop.to_string(), converted);
551 }
552 }
553 }
554
555 if let Some(font_size) = styles.get("fontSize").cloned() {
557 if let Some(serde_json::Value::Number(n)) = parse_and_convert_to_px(&font_size, density).as_ref() {
558 let sp_value = n.as_f64().unwrap_or(14.0) as f32 / density * scaled_density;
560 styles.insert("fontSize".to_string(), serde_json::json!(sp_value));
561 }
562 }
563
564 if let Some(flex_wrap) = styles.get("flexWrap") {
566 if flex_wrap.as_str() == Some("wrap") {
567 styles.insert("androidFlexWrap".to_string(), serde_json::json!(true));
568 }
569 }
570
571 if let Some(opacity) = styles.get("opacity").cloned() {
573 styles.insert("androidAlpha".to_string(), opacity);
574 }
575
576 let is_horizontal = styles.get("androidOrientation").and_then(|v| v.as_str()) == Some("horizontal");
577 let mut gravity_parts = Vec::new();
578
579 if let Some(align_items) = styles.get("alignItems") {
581 let part = match align_items.as_str() {
582 Some("center") => if is_horizontal { "center_vertical" } else { "center_horizontal" },
583 Some("flex-start") | Some("start") => if is_horizontal { "top" } else { "start" },
584 Some("flex-end") | Some("end") => if is_horizontal { "bottom" } else { "end" },
585 Some("stretch") => if is_horizontal { "fill_vertical" } else { "fill_horizontal" },
586 _ => ""
587 };
588 if !part.is_empty() {
589 gravity_parts.push(part);
590 }
591 }
592
593 if let Some(justify) = styles.get("justifyContent") {
595 let part = match justify.as_str() {
596 Some("center") => if is_horizontal { "center_horizontal" } else { "center_vertical" },
597 Some("flex-start") | Some("start") => if is_horizontal { "start" } else { "top" },
598 Some("flex-end") | Some("end") => if is_horizontal { "end" } else { "bottom" },
599 _ => ""
600 };
601 if !part.is_empty() {
602 gravity_parts.push(part);
603 }
604
605 let layout_gravity = match justify.as_str() {
607 Some("center") => "center_horizontal",
608 Some("flex-start") | Some("start") => "start",
609 Some("flex-end") | Some("end") => "end",
610 Some("space-between") | Some("between") => "space_between",
611 Some("space-around") | Some("around") => "space_around",
612 _ => ""
613 };
614 if !layout_gravity.is_empty() {
615 styles.insert("androidLayoutGravity".to_string(), serde_json::json!(layout_gravity));
616 }
617 }
618
619 if !gravity_parts.is_empty() {
620 let gravity = if gravity_parts.contains(&"center_vertical") && gravity_parts.contains(&"center_horizontal") {
621 "center".to_string()
622 } else {
623 gravity_parts.join("|")
624 };
625 styles.insert("androidGravity".to_string(), serde_json::json!(gravity));
626 }
627
628 if let Some(serde_json::Value::String(border)) = styles.get("border").cloned() {
630 let parts: Vec<&str> = border.split_whitespace().collect();
631 for part in parts {
632 if part.ends_with("px") {
633 if let Ok(w) = part.trim_end_matches("px").parse::<f32>() {
634 styles.insert("borderWidth".to_string(), serde_json::json!(dp_to_px(w, density)));
635 }
636 } else if part.starts_with('#') {
637 styles.insert("borderColor".to_string(), serde_json::json!(part));
638 }
639 }
640 }
641
642 if let Some(serde_json::Value::String(shadow)) = styles.get("boxShadow").cloned() {
644 if !shadow.is_empty() {
645 let elevation = if shadow.contains("20px") { 24 }
646 else if shadow.contains("15px") { 16 }
647 else if shadow.contains("10px") { 8 }
648 else { 4 };
649 styles.insert("elevation".to_string(), serde_json::json!(dp_to_px(elevation as f32, density)));
650 }
651 }
652
653 if let Some(overflow_x) = styles.get("overflowX") {
655 if overflow_x.as_str() == Some("auto") || overflow_x.as_str() == Some("scroll") {
656 styles.insert("androidScrollHorizontal".to_string(), serde_json::json!(true));
657 }
658 }
659 if let Some(overflow_y) = styles.get("overflowY") {
660 if overflow_y.as_str() == Some("auto") || overflow_y.as_str() == Some("scroll") {
661 styles.insert("androidScrollVertical".to_string(), serde_json::json!(true));
662 }
663 }
664
665 if let Some(text_align) = styles.get("textAlign") {
667 let gravity = match text_align.as_str() {
668 Some("center") => "center_horizontal",
669 Some("right") | Some("end") => "end",
670 Some("left") | Some("start") => "start",
671 _ => ""
672 };
673 if !gravity.is_empty() {
674 styles.insert("androidTextGravity".to_string(), serde_json::json!(gravity));
675 }
676 }
677
678 if let Some(object_fit) = styles.get("objectFit") {
680 let scale_type = match object_fit.as_str() {
681 Some("cover") => "center_crop",
682 Some("contain") => "fit_center",
683 Some("fill") => "fit_xy",
684 Some("none") => "center",
685 Some("scale-down") => "center_inside",
686 _ => ""
687 };
688 if !scale_type.is_empty() {
689 styles.insert("androidScaleType".to_string(), serde_json::json!(scale_type));
690 }
691 }
692
693 if let Some(h) = styles.get("height").cloned() {
695 if h.as_str() == Some("100%") {
696 styles.insert("height".to_string(), serde_json::json!("match_parent"));
697 }
698 }
699 if let Some(w) = styles.get("width").cloned() {
700 if w.as_str() == Some("100%") {
701 styles.insert("width".to_string(), serde_json::json!("match_parent"));
702 }
703 }
704
705 if styles.contains_key("flex") || styles.contains_key("flexGrow") {
709 if !styles.contains_key("width") {
711 styles.insert("width".to_string(), serde_json::json!("wrap_content"));
712 }
713 if !styles.contains_key("height") {
714 styles.insert("height".to_string(), serde_json::json!("wrap_content"));
715 }
716 }
717
718 if let Some(font_weight) = styles.get("fontWeight") {
720 let is_bold = match font_weight {
721 serde_json::Value::String(s) => s.contains("bold") || s == "600" || s == "700" || s == "500",
722 serde_json::Value::Number(n) => {
723 let weight = n.as_i64().unwrap_or(400);
724 weight >= 500
725 }
726 _ => false
727 };
728 if is_bold {
729 styles.insert("androidTypefaceStyle".to_string(), serde_json::json!("bold"));
730 }
731 }
732
733 if let Some(box_shadow) = styles.get("boxShadow") {
735 if let Some(shadow_str) = box_shadow.as_str() {
736 if !shadow_str.is_empty() {
737 let elevation_dp = if shadow_str.contains("20px") { 24.0 }
738 else if shadow_str.contains("15px") { 16.0 }
739 else if shadow_str.contains("10px") { 8.0 }
740 else if shadow_str.contains("5px") { 4.0 }
741 else { 4.0 };
742 styles.insert("elevation".to_string(), serde_json::json!(dp_to_px(elevation_dp, density)));
743 }
744 }
745 }
746
747 styles
748 }
749
750 fn theme_chain(&self) -> Vec<String> {
754 let mut chain = Vec::new();
755 let default_name = if self.themes.contains_key(&self.default_theme) {
757 self.default_theme.clone()
758 } else if let Some((k, _)) = self.themes.first() { k.clone() } else { return chain };
759 let mut current_name = if self.themes.contains_key(&self.current_theme) {
760 self.current_theme.clone()
761 } else { default_name.clone() };
762 let mut seen: IndexSet<String> = IndexSet::new();
764 while !seen.contains(¤t_name) {
765 seen.insert(current_name.clone());
766 chain.push(current_name.clone());
767 let inherits = self.themes.get(¤t_name).and_then(|t| t.inherits.clone());
769 if let Some(p) = inherits {
770 current_name = p;
771 } else {
772 break;
773 }
774 }
775 if !chain.iter().any(|n| n == &default_name) {
776 chain.push(default_name);
777 }
778 chain
779 }
780
781 fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
784 let mut selectors: SelectorStyles = SelectorStyles::new();
785 let mut vars: IndexMap<String, String> = IndexMap::new();
786 for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
788 let chain = self.theme_chain();
790 for name in chain.into_iter().rev() {
791 if let Some(entry) = self.themes.get(&name) {
792 for (sel, props) in entry.selectors.iter() {
794 if sel.contains(',') {
796 for s in sel.split(',') {
797 let s = s.trim();
798 if s.is_empty() { continue; }
799 let e = selectors.entry(s.to_string()).or_default();
800 merge_props(e, props);
801 }
802 } else {
803 let e = selectors.entry(sel.clone()).or_default();
804 merge_props(e, props);
805 }
806 }
807 for (k, v) in entry.variables.iter() {
809 vars.insert(k.clone(), v.clone());
810 }
811 }
812 }
813 (selectors, vars)
814 }
815
816 pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
818 let mut bps: IndexMap<String, String> = IndexMap::new();
819 for (k, v) in self.breakpoints.iter() { bps.insert(k.clone(), v.clone()); }
821 let chain = self.theme_chain();
822 for name in chain.into_iter().rev() {
823 if let Some(entry) = self.themes.get(&name) {
824 for (k, v) in entry.breakpoints.iter() {
825 bps.insert(k.clone(), v.clone());
826 }
827 }
828 }
829 bps
830 }
831}
832
833fn split_tag_class_key(key: &str) -> Option<(String, String)> {
834 let mut it = key.splitn(2, '|');
835 let t = it.next()?.to_string();
836 let c = it.next()?.to_string();
837 if t.is_empty() || c.is_empty() { return None; }
838 Some((t, c))
839}
840
841fn strip_hover_suffix(selector: &str) -> (&str, bool) {
842 if let Some(stripped) = selector.strip_suffix(":hover") { (stripped, true) } else { (selector, false) }
843}
844
845fn should_emit_selector(sel: &str, used_tags: &IndexSet<String>, used_classes: &IndexSet<String>, used_tag_classes: &IndexSet<String>) -> bool {
846 let (base, _hover) = strip_hover_suffix(sel);
848
849 if is_simple_tag(base) {
851 return used_tags.contains(base) || used_tag_classes.iter().any(|k| k.split('|').next() == Some(base));
852 }
853
854 if let Some(class_name) = base.strip_prefix('.') {
856 return used_classes.contains(class_name) || used_tag_classes.iter().any(|k| k.ends_with(&format!("|{}", class_name)));
858 }
859
860 if let Some((tag, class_name)) = split_tag_class_selector(base) {
862 let key = format!("{}|{}", tag, class_name);
863 return used_tag_classes.contains(&key) || (used_tags.contains(&tag) && used_classes.contains(&class_name));
864 }
865
866 false
868}
869
870fn is_simple_tag(s: &str) -> bool {
871 let mut chars = s.chars();
873 match chars.next() { Some(c) if c.is_ascii_alphabetic() => {}, _ => return false }
874 chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
875}
876
877fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
878 let mut parts = s.splitn(2, '.');
880 let tag = parts.next()?.to_string();
881 let class_name = parts.next()?.to_string();
882 if tag.is_empty() || class_name.is_empty() { return None; }
883 Some((tag, class_name))
884}
885
886#[cfg(target_arch = "wasm32")]
888#[wasm_bindgen]
889pub fn render_css_for_web(state_json: &str) -> String {
890 match serde_json::from_str::<State>(state_json) {
891 Ok(s) => s.css_for_web(),
892 Err(_) => "".into(),
893 }
894}
895
896#[cfg(target_arch = "wasm32")]
897#[wasm_bindgen]
898pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
899 let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
900 match serde_json::from_str::<State>(state_json) {
901 Ok(s) => serde_json::to_string(&s.android_styles_for(selector, &classes)).unwrap_or_else(|_| "{}".into()),
902 Err(_) => "{}".into(),
903 }
904}
905
906#[cfg(target_arch = "wasm32")]
907#[wasm_bindgen]
908pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
909 get_android_styles(state_json, selector, classes_json)
910}
911
912#[cfg(target_arch = "wasm32")]
914#[wasm_bindgen]
915pub fn get_version() -> String {
916 env!("CARGO_PKG_VERSION").to_string()
918}
919
920pub fn version() -> &'static str {
922 env!("CARGO_PKG_VERSION")
923}
924
925#[cfg(target_arch = "wasm32")]
927#[wasm_bindgen]
928pub fn get_default_state_json() -> String {
929 let st = bundled_state();
930 match serde_json::to_string(&st.to_json()) {
931 Ok(s) => s,
932 Err(_) => "{}".to_string(),
933 }
934}
935
936#[cfg(target_arch = "wasm32")]
940#[wasm_bindgen]
941pub fn register_theme_json(state_json: &str, theme_json: &str) -> String {
942 match (serde_json::from_str::<State>(state_json), serde_json::from_str::<serde_json::Value>(theme_json)) {
943 (Ok(mut state), Ok(theme_obj)) => {
944 if let (Some(name), Some(theme_entry)) = (theme_obj.get("name"), theme_obj.get("theme")) {
945 if let Ok(entry) = serde_json::from_value::<ThemeEntry>(theme_entry.clone()) {
946 let theme_name = name.as_str().unwrap_or("").to_string();
947 if !theme_name.is_empty() {
948 state.themes.insert(theme_name, entry);
949 }
950 }
951 }
952 match serde_json::to_string(&state.to_json()) {
953 Ok(s) => s,
954 Err(_) => "{}".to_string(),
955 }
956 }
957 _ => "{}".to_string(),
958 }
959}
960
961#[cfg(target_arch = "wasm32")]
963#[wasm_bindgen]
964pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
965 match serde_json::from_str::<State>(state_json) {
966 Ok(mut state) => {
967 if state.themes.contains_key(theme_name) {
968 state.default_theme = theme_name.to_string();
969 state.current_theme = theme_name.to_string();
970 }
971 match serde_json::to_string(&state.to_json()) {
972 Ok(s) => s,
973 Err(_) => "{}".to_string(),
974 }
975 }
976 _ => "{}".to_string(),
977 }
978}
979
980#[cfg(target_arch = "wasm32")]
983#[wasm_bindgen]
984pub fn get_theme_list_json(state_json: &str) -> String {
985 match serde_json::from_str::<State>(state_json) {
986 Ok(state) => {
987 let themes: Vec<serde_json::Value> = state.themes.iter().map(|(key, entry)| {
988 json!({
989 "key": key,
990 "name": entry.name.as_ref().unwrap_or(key)
991 })
992 }).collect();
993 serde_json::to_string(&themes).unwrap_or_else(|_| "[]".to_string())
994 }
995 _ => "[]".to_string(),
996 }
997}
998
999fn merge_props(into: &mut CssProps, from: &CssProps) {
1000 for (k, v) in from.iter() {
1001 into.insert(k.clone(), v.clone());
1002 }
1003}
1004
1005fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
1008 let mut buf = String::new();
1009 for (k, v) in props.iter() {
1010 buf.push_str(k);
1011 buf.push(':');
1012 let val = if v.is_string() {
1013 let s = v.as_str().unwrap();
1014 resolve_vars(s, vars)
1015 } else {
1016 v.to_string()
1017 };
1018 buf.push_str(&val);
1019 if !val.ends_with(';') {
1020 buf.push(';');
1021 }
1022 }
1023 buf
1024}
1025
1026fn parse_var_references(input: &str) -> Vec<(usize, usize, String)> {
1030 let mut results = Vec::new();
1031 let bytes = input.as_bytes();
1032 let mut i = 0;
1033
1034 while i < bytes.len() {
1035 if i + 4 <= bytes.len() && &bytes[i..i+4] == b"var(" {
1037 let start = i;
1038 i += 4;
1039
1040 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
1042 i += 1;
1043 }
1044
1045 let has_prefix = i + 2 <= bytes.len() && &bytes[i..i+2] == b"--";
1047 if has_prefix {
1048 i += 2;
1049 }
1050
1051 let name_start = i;
1053 while i < bytes.len() {
1054 let c = bytes[i];
1055 if (c >= b'a' && c <= b'z') || (c >= b'A' && c <= b'Z') ||
1056 (c >= b'0' && c <= b'9') || c == b'_' || c == b'.' || c == b'-' {
1057 i += 1;
1058 } else {
1059 break;
1060 }
1061 }
1062
1063 let name_end = i;
1064 if name_start < name_end {
1065 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
1067 i += 1;
1068 }
1069
1070 if i < bytes.len() && bytes[i] == b')' {
1072 let end = i + 1;
1073 let var_name = std::str::from_utf8(&bytes[name_start..name_end])
1074 .unwrap_or("").to_string();
1075 results.push((start, end, var_name));
1076 i = end;
1077 continue;
1078 }
1079 }
1080 }
1081 i += 1;
1082 }
1083
1084 results
1085}
1086
1087static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> = Lazy::new(|| {
1089 let mut colors = IndexMap::new();
1090
1091 let mut slate = IndexMap::new();
1092 slate.insert("50", "#f8fafc"); slate.insert("100", "#f1f5f9"); slate.insert("200", "#e2e8f0");
1093 slate.insert("300", "#cbd5e1"); slate.insert("400", "#94a3b8"); slate.insert("500", "#64748b");
1094 slate.insert("600", "#475569"); slate.insert("700", "#334155"); slate.insert("800", "#1e293b");
1095 slate.insert("900", "#0f172a"); slate.insert("950", "#020617");
1096 colors.insert("slate", slate);
1097
1098 let mut gray = IndexMap::new();
1099 gray.insert("50", "#f9fafb"); gray.insert("100", "#f3f4f6"); gray.insert("200", "#e5e7eb");
1100 gray.insert("300", "#d1d5db"); gray.insert("400", "#9ca3af"); gray.insert("500", "#6b7280");
1101 gray.insert("600", "#4b5563"); gray.insert("700", "#374151"); gray.insert("800", "#1f2937");
1102 gray.insert("900", "#111827"); gray.insert("950", "#030712");
1103 colors.insert("gray", gray);
1104
1105 let mut zinc = IndexMap::new();
1106 zinc.insert("50", "#fafafa"); zinc.insert("100", "#f4f4f5"); zinc.insert("200", "#e4e4e7");
1107 zinc.insert("300", "#d4d4d8"); zinc.insert("400", "#a1a1aa"); zinc.insert("500", "#71717a");
1108 zinc.insert("600", "#52525b"); zinc.insert("700", "#3f3f46"); zinc.insert("800", "#27272a");
1109 zinc.insert("900", "#18181b"); zinc.insert("950", "#09090b");
1110 colors.insert("zinc", zinc);
1111
1112 let mut neutral = IndexMap::new();
1113 neutral.insert("50", "#fafafa"); neutral.insert("100", "#f5f5f5"); neutral.insert("200", "#e5e5e5");
1114 neutral.insert("300", "#d4d4d4"); neutral.insert("400", "#a3a3a3"); neutral.insert("500", "#737373");
1115 neutral.insert("600", "#525252"); neutral.insert("700", "#404040"); neutral.insert("800", "#262626");
1116 neutral.insert("900", "#171717"); neutral.insert("950", "#0a0a0a");
1117 colors.insert("neutral", neutral);
1118
1119 let mut stone = IndexMap::new();
1120 stone.insert("50", "#fafaf9"); stone.insert("100", "#f5f5f4"); stone.insert("200", "#e7e5e4");
1121 stone.insert("300", "#d6d3d1"); stone.insert("400", "#a8a29e"); stone.insert("500", "#78716c");
1122 stone.insert("600", "#57534e"); stone.insert("700", "#44403c"); stone.insert("800", "#292524");
1123 stone.insert("900", "#1c1917"); stone.insert("950", "#0c0a09");
1124 colors.insert("stone", stone);
1125
1126 let mut red = IndexMap::new();
1127 red.insert("50", "#fef2f2"); red.insert("100", "#fee2e2"); red.insert("200", "#fecaca");
1128 red.insert("300", "#fca5a5"); red.insert("400", "#f87171"); red.insert("500", "#ef4444");
1129 red.insert("600", "#dc2626"); red.insert("700", "#b91c1c"); red.insert("800", "#991b1b");
1130 red.insert("900", "#7f1d1d"); red.insert("950", "#450a0a");
1131 colors.insert("red", red);
1132
1133 let mut orange = IndexMap::new();
1134 orange.insert("50", "#fff7ed"); orange.insert("100", "#ffedd5"); orange.insert("200", "#fed7aa");
1135 orange.insert("300", "#fdba74"); orange.insert("400", "#fb923c"); orange.insert("500", "#f97316");
1136 orange.insert("600", "#ea580c"); orange.insert("700", "#c2410c"); orange.insert("800", "#9a3412");
1137 orange.insert("900", "#7c2d12"); orange.insert("950", "#431407");
1138 colors.insert("orange", orange);
1139
1140 let mut amber = IndexMap::new();
1141 amber.insert("50", "#fffbeb"); amber.insert("100", "#fef3c7"); amber.insert("200", "#fde68a");
1142 amber.insert("300", "#fcd34d"); amber.insert("400", "#fbbf24"); amber.insert("500", "#f59e0b");
1143 amber.insert("600", "#d97706"); amber.insert("700", "#b45309"); amber.insert("800", "#92400e");
1144 amber.insert("900", "#78350f"); amber.insert("950", "#451a03");
1145 colors.insert("amber", amber);
1146
1147 let mut blue = IndexMap::new();
1148 blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
1149 blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
1150 blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
1151 blue.insert("900", "#1e3a8a"); blue.insert("950", "#0b1c52");
1152 colors.insert("blue", blue);
1153
1154 let mut lime = IndexMap::new();
1155 lime.insert("50", "#f7fee7"); lime.insert("100", "#ecfccb"); lime.insert("200", "#d9f99d");
1156 lime.insert("300", "#bef264"); lime.insert("400", "#a3e635"); lime.insert("500", "#84cc16");
1157 lime.insert("600", "#65a30d"); lime.insert("700", "#4d7c0f"); lime.insert("800", "#3f6212");
1158 lime.insert("900", "#365314"); lime.insert("950", "#1a2e05");
1159 colors.insert("lime", lime);
1160
1161 let mut green = IndexMap::new();
1162 green.insert("50", "#f0fdf4"); green.insert("100", "#dcfce7"); green.insert("200", "#bbf7d0");
1163 green.insert("300", "#86efac"); green.insert("400", "#4ade80"); green.insert("500", "#22c55e");
1164 green.insert("600", "#16a34a"); green.insert("700", "#15803d"); green.insert("800", "#166534");
1165 green.insert("900", "#14532d"); green.insert("950", "#052e16");
1166 colors.insert("green", green);
1167
1168 let mut emerald = IndexMap::new();
1169 emerald.insert("50", "#ecfdf5"); emerald.insert("100", "#d1fae5"); emerald.insert("200", "#a7f3d0");
1170 emerald.insert("300", "#6ee7b7"); emerald.insert("400", "#34d399"); emerald.insert("500", "#10b981");
1171 emerald.insert("600", "#059669"); emerald.insert("700", "#047857"); emerald.insert("800", "#065f46");
1172 emerald.insert("900", "#064e3b"); emerald.insert("950", "#022c22");
1173 colors.insert("emerald", emerald);
1174
1175 let mut teal = IndexMap::new();
1176 teal.insert("50", "#f0fdfa"); teal.insert("100", "#ccfbf1"); teal.insert("200", "#99f6e4");
1177 teal.insert("300", "#5eead4"); teal.insert("400", "#2dd4bf"); teal.insert("500", "#14b8a6");
1178 teal.insert("600", "#0d9488"); teal.insert("700", "#0f766e"); teal.insert("800", "#115e59");
1179 teal.insert("900", "#134e4a"); teal.insert("950", "#042f2e");
1180 colors.insert("teal", teal);
1181
1182 let mut cyan = IndexMap::new();
1183 cyan.insert("50", "#ecfeff"); cyan.insert("100", "#cffafe"); cyan.insert("200", "#a5f3fc");
1184 cyan.insert("300", "#67e8f9"); cyan.insert("400", "#22d3ee"); cyan.insert("500", "#06b6d4");
1185 cyan.insert("600", "#0891b2"); cyan.insert("700", "#0e7490"); cyan.insert("800", "#155e75");
1186 cyan.insert("900", "#164e63"); cyan.insert("950", "#083344");
1187 colors.insert("cyan", cyan);
1188
1189 let mut sky = IndexMap::new();
1190 sky.insert("50", "#f0f9ff"); sky.insert("100", "#e0f2fe"); sky.insert("200", "#bae6fd");
1191 sky.insert("300", "#7dd3fc"); sky.insert("400", "#38bdf8"); sky.insert("500", "#0ea5e9");
1192 sky.insert("600", "#0284c7"); sky.insert("700", "#0369a1"); sky.insert("800", "#075985");
1193 sky.insert("900", "#0c4a6e"); sky.insert("950", "#082f49");
1194 colors.insert("sky", sky);
1195
1196 let mut blue = IndexMap::new();
1197 blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
1198 blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
1199 blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
1200 blue.insert("900", "#1e3a8a"); blue.insert("950", "#172554");
1201 colors.insert("blue", blue);
1202
1203 let mut indigo = IndexMap::new();
1204 indigo.insert("50", "#eef2ff"); indigo.insert("100", "#e0e7ff"); indigo.insert("200", "#c7d2fe");
1205 indigo.insert("300", "#a5b4fc"); indigo.insert("400", "#818cf8"); indigo.insert("500", "#6366f1");
1206 indigo.insert("600", "#4f46e5"); indigo.insert("700", "#4338ca"); indigo.insert("800", "#3730a3");
1207 indigo.insert("900", "#312e81"); indigo.insert("950", "#1e1b4b");
1208 colors.insert("indigo", indigo);
1209
1210 let mut violet = IndexMap::new();
1211 violet.insert("50", "#f5f3ff"); violet.insert("100", "#ede9fe"); violet.insert("200", "#ddd6fe");
1212 violet.insert("300", "#c4b5fd"); violet.insert("400", "#a78bfa"); violet.insert("500", "#8b5cf6");
1213 violet.insert("600", "#7c3aed"); violet.insert("700", "#6d28d9"); violet.insert("800", "#5b21b6");
1214 violet.insert("900", "#4c1d95"); violet.insert("950", "#2e1065");
1215 colors.insert("violet", violet);
1216
1217 let mut purple = IndexMap::new();
1218 purple.insert("50", "#faf5ff"); purple.insert("100", "#f3e8ff"); purple.insert("200", "#e9d5ff");
1219 purple.insert("300", "#d8b4fe"); purple.insert("400", "#c084fc"); purple.insert("500", "#a855f7");
1220 purple.insert("600", "#9333ea"); purple.insert("700", "#7e22ce"); purple.insert("800", "#6b21a8");
1221 purple.insert("900", "#581c87"); purple.insert("950", "#3b0764");
1222 colors.insert("purple", purple);
1223
1224 let mut fuchsia = IndexMap::new();
1225 fuchsia.insert("50", "#fdf4ff"); fuchsia.insert("100", "#fae8ff"); fuchsia.insert("200", "#f5d0fe");
1226 fuchsia.insert("300", "#f0abfc"); fuchsia.insert("400", "#e879f9"); fuchsia.insert("500", "#d946ef");
1227 fuchsia.insert("600", "#c026d3"); fuchsia.insert("700", "#a21caf"); fuchsia.insert("800", "#86198f");
1228 fuchsia.insert("900", "#701a75"); fuchsia.insert("950", "#4a044e");
1229 colors.insert("fuchsia", fuchsia);
1230
1231 let mut pink = IndexMap::new();
1232 pink.insert("50", "#fdf2f8"); pink.insert("100", "#fce7f3"); pink.insert("200", "#fbcfe8");
1233 pink.insert("300", "#f9a8d4"); pink.insert("400", "#f472b6"); pink.insert("500", "#ec4899");
1234 pink.insert("600", "#db2777"); pink.insert("700", "#be185d"); pink.insert("800", "#9d174d");
1235 pink.insert("900", "#831843"); pink.insert("950", "#500724");
1236 colors.insert("pink", pink);
1237
1238 let mut rose = IndexMap::new();
1239 rose.insert("50", "#fff1f2"); rose.insert("100", "#ffe4e6"); rose.insert("200", "#fecdd3");
1240 rose.insert("300", "#fda4af"); rose.insert("400", "#fb7185"); rose.insert("500", "#f43f5e");
1241 rose.insert("600", "#e11d48"); rose.insert("700", "#be123c"); rose.insert("800", "#9f1239");
1242 rose.insert("900", "#881337"); rose.insert("950", "#4c0519");
1243 colors.insert("rose", rose);
1244
1245 colors
1246});
1247
1248fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
1249 let var_refs = parse_var_references(input);
1250
1251 if var_refs.is_empty() {
1252 if input.starts_with('$') {
1254 if let Some(val) = vars.get(&input[1..]) {
1255 return val.clone();
1256 }
1257 }
1258 return input.to_string();
1259 }
1260
1261 let mut out = input.to_string();
1263 for (start, end, var_name) in var_refs.iter().rev() {
1264 if let Some(val) = vars.get(var_name) {
1265 out.replace_range(*start..*end, val);
1266 }
1267 }
1268
1269 if out.starts_with('$') {
1271 if let Some(val) = vars.get(&out[1..]) {
1272 return val.clone();
1273 }
1274 }
1275
1276 out
1277}
1278
1279fn camel_case(name: &str) -> String {
1280 let mut out = String::new();
1281 let mut upper = false;
1282 for ch in name.chars() {
1283 if ch == '-' {
1284 upper = true;
1285 continue;
1286 }
1287 if upper {
1288 out.extend(ch.to_uppercase());
1289 upper = false;
1290 } else {
1291 out.push(ch);
1292 }
1293 }
1294 out
1295}
1296
1297fn css_value_to_android(
1298 value: &serde_json::Value,
1299 vars: &IndexMap<String, String>,
1300) -> serde_json::Value {
1301 match value {
1302 serde_json::Value::String(s) => {
1303 let s2 = resolve_vars(s, vars);
1304 if let Some(n) = s2.strip_suffix("px") {
1305 if let Ok(parsed) = n.trim().parse::<f64>() {
1306 return json!(parsed);
1307 }
1308 }
1309 json!(s2)
1310 }
1311 _ => value.clone(),
1312 }
1313}
1314
1315fn merge_android_props(
1316 into: &mut IndexMap<String, serde_json::Value>,
1317 css_props: &CssProps,
1318 vars: &IndexMap<String, String>,
1319) {
1320 for (k, v) in css_props.iter() {
1321 let val = css_value_to_android(v, vars);
1322
1323 match k.as_str() {
1324 "padding" => {
1325 into.insert("paddingTop".to_string(), val.clone());
1326 into.insert("paddingBottom".to_string(), val.clone());
1327 into.insert("paddingLeft".to_string(), val.clone());
1328 into.insert("paddingRight".to_string(), val.clone());
1329 into.insert("paddingHorizontal".to_string(), val.clone());
1330 into.insert("paddingVertical".to_string(), val.clone());
1331 into.insert("padding".to_string(), val);
1332 }
1333 "padding-horizontal" | "paddingHorizontal" => {
1334 into.insert("paddingLeft".to_string(), val.clone());
1335 into.insert("paddingRight".to_string(), val.clone());
1336 into.insert("paddingHorizontal".to_string(), val);
1337 }
1338 "padding-vertical" | "paddingVertical" => {
1339 into.insert("paddingTop".to_string(), val.clone());
1340 into.insert("paddingBottom".to_string(), val.clone());
1341 into.insert("paddingVertical".to_string(), val);
1342 }
1343 "margin" => {
1344 into.insert("marginTop".to_string(), val.clone());
1345 into.insert("marginBottom".to_string(), val.clone());
1346 into.insert("marginLeft".to_string(), val.clone());
1347 into.insert("marginRight".to_string(), val.clone());
1348 into.insert("marginHorizontal".to_string(), val.clone());
1349 into.insert("marginVertical".to_string(), val.clone());
1350 into.insert("margin".to_string(), val);
1351 }
1352 "margin-horizontal" | "marginHorizontal" => {
1353 into.insert("marginLeft".to_string(), val.clone());
1354 into.insert("marginRight".to_string(), val.clone());
1355 into.insert("marginHorizontal".to_string(), val);
1356 }
1357 "margin-vertical" | "marginVertical" => {
1358 into.insert("marginTop".to_string(), val.clone());
1359 into.insert("marginBottom".to_string(), val.clone());
1360 into.insert("marginVertical".to_string(), val);
1361 }
1362 "border-radius" | "borderRadius" => {
1363 into.insert("borderTopLeftRadius".to_string(), val.clone());
1364 into.insert("borderTopRightRadius".to_string(), val.clone());
1365 into.insert("borderBottomLeftRadius".to_string(), val.clone());
1366 into.insert("borderBottomRightRadius".to_string(), val.clone());
1367 into.insert("borderRadius".to_string(), val);
1368 }
1369 "background-color" => { into.insert("backgroundColor".to_string(), val); }
1370 "text-align" => { into.insert("textAlign".to_string(), val); }
1371 "flex-direction" | "flexDirection" => {
1372 let orientation = if val.as_str() == Some("column") || val.as_str() == Some("column-reverse") {
1373 "vertical"
1374 } else {
1375 "horizontal"
1376 };
1377 into.insert("androidOrientation".to_string(), serde_json::json!(orientation));
1378 into.insert("flexDirection".to_string(), val);
1379 }
1380 _ => {
1381 into.insert(camel_case(k), val);
1382 }
1383 }
1384 }
1385}
1386
1387fn dynamic_css_properties_for_class(class: &str, vars: &IndexMap<String, String>) -> Option<CssProps> {
1388 match class {
1390 "block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("block")); return Some(p); }
1391 "inline-block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-block")); return Some(p); }
1392 "inline" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline")); return Some(p); }
1393 "inline-flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-flex")); return Some(p); }
1394 "grid" => { let mut p = CssProps::new(); p.insert("display".into(), json!("grid")); return Some(p); }
1395 "hidden" => { let mut p = CssProps::new(); p.insert("display".into(), json!("none")); return Some(p); }
1396 _ => {}
1397 }
1398 match class {
1400 "flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); return Some(p); }
1401 "flex-row" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flexDirection".into(), json!("row")); return Some(p); }
1402 "flex-col" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flexDirection".into(), json!("column")); return Some(p); }
1403 "flex-wrap" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("wrap")); return Some(p); }
1404 "flex-nowrap" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("nowrap")); return Some(p); }
1405 "flex-wrap-reverse" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("wrap-reverse")); return Some(p); }
1406 "flex-1" => { let mut p = CssProps::new(); p.insert("flex".into(), json!(1)); return Some(p); }
1407 "w-full" => { let mut p = CssProps::new(); p.insert("width".into(), json!("match_parent")); return Some(p); }
1408 "h-full" => { let mut p = CssProps::new(); p.insert("height".into(), json!("match_parent")); return Some(p); }
1409 _ => {}
1410 }
1411 if let Some(value) = class.strip_prefix("z-") {
1412 if let Ok(z) = value.parse::<i32>() {
1413 let mut p = CssProps::new();
1414 p.insert("elevation".into(), json!(z));
1415 return Some(p);
1416 }
1417 }
1418 if let Some(rest) = class.strip_prefix("items-") {
1419 let mut p = CssProps::new();
1420 let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "stretch" => "stretch", other => other };
1421 p.insert("align-items".into(), json!(v));
1422 return Some(p);
1423 }
1424 if let Some(rest) = class.strip_prefix("justify-") {
1425 let mut p = CssProps::new();
1426 let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "between" => "space-between", "around" => "space-around", "evenly" => "space-evenly", other => other };
1427 p.insert("justify-content".into(), json!(v));
1428 return Some(p);
1429 }
1430 if let Some(value) = class.strip_prefix("p-") {
1431 return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
1432 }
1433 if let Some(value) = class.strip_prefix("px-") {
1434 return parse_tailwind_spacing(value, &|px| padding_props(&["padding-left", "padding-right"], px));
1435 }
1436 if let Some(value) = class.strip_prefix("py-") {
1437 return parse_tailwind_spacing(value, &|px| padding_props(&["padding-top", "padding-bottom"], px));
1438 }
1439 for &(prefix, prop) in &[("pt-", "padding-top"), ("pr-", "padding-right"), ("pb-", "padding-bottom"), ("pl-", "padding-left")] {
1440 if let Some(value) = class.strip_prefix(prefix) {
1441 return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
1442 }
1443 }
1444 if let Some(value) = class.strip_prefix("m-") {
1446 if value == "auto" {
1447 let mut p = CssProps::new();
1448 p.insert("margin".into(), json!("auto"));
1449 return Some(p);
1450 }
1451 return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
1452 }
1453 if let Some(value) = class.strip_prefix("mx-") {
1454 if value == "auto" {
1455 let mut p = CssProps::new();
1456 p.insert("margin-left".into(), json!("auto"));
1457 p.insert("margin-right".into(), json!("auto"));
1458 return Some(p);
1459 }
1460 return parse_tailwind_spacing(value, &|px| margin_props(&["margin-left", "margin-right"], px));
1461 }
1462 if let Some(value) = class.strip_prefix("my-") {
1463 if value == "auto" {
1464 let mut p = CssProps::new();
1465 p.insert("margin-top".into(), json!("auto"));
1466 p.insert("margin-bottom".into(), json!("auto"));
1467 return Some(p);
1468 }
1469 return parse_tailwind_spacing(value, &|px| margin_props(&["margin-top", "margin-bottom"], px));
1470 }
1471 for &(prefix, prop) in &[("mt-", "margin-top"), ("mr-", "margin-right"), ("mb-", "margin-bottom"), ("ml-", "margin-left")] {
1472 if let Some(value) = class.strip_prefix(prefix) {
1473 if value == "auto" {
1474 let mut p = CssProps::new();
1475 p.insert(prop.into(), json!("auto"));
1476 return Some(p);
1477 }
1478 return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
1479 }
1480 }
1481 if let Some(value) = class.strip_prefix("gap-") {
1483 if !value.starts_with("x-") && !value.starts_with("y-") {
1484 return parse_tailwind_spacing(value, &|px| {
1485 let mut props = CssProps::new();
1486 props.insert("gap".into(), json!(format!("{}px", px)));
1487 props
1488 });
1489 }
1490 }
1491 if let Some(value) = class.strip_prefix("gap-x-") {
1492 return parse_tailwind_spacing(value, &|px| {
1493 let mut props = CssProps::new();
1494 props.insert("column-gap".into(), json!(format!("{}px", px)));
1495 props
1496 });
1497 }
1498 if let Some(value) = class.strip_prefix("gap-y-") {
1499 return parse_tailwind_spacing(value, &|px| {
1500 let mut props = CssProps::new();
1501 props.insert("row-gap".into(), json!(format!("{}px", px)));
1502 props
1503 });
1504 }
1505 if let Some(value) = class.strip_prefix("space-x-") {
1507 return parse_tailwind_spacing(value, &|px| {
1508 let mut props = CssProps::new();
1509 props.insert("--space-x".into(), json!(format!("{}px", px)));
1512 props
1513 });
1514 }
1515 if let Some(value) = class.strip_prefix("space-y-") {
1516 return parse_tailwind_spacing(value, &|px| {
1517 let mut props = CssProps::new();
1518 props.insert("--space-y".into(), json!(format!("{}px", px)));
1519 props
1520 });
1521 }
1522 match class {
1524 "font-thin" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("100")); return Some(p); }
1525 "font-extralight" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("200")); return Some(p); }
1526 "font-light" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("300")); return Some(p); }
1527 "font-normal" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("400")); return Some(p); }
1528 "font-medium" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("500")); return Some(p); }
1529 "font-semibold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("600")); return Some(p); }
1530 "font-bold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("700")); return Some(p); }
1531 "font-extrabold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("800")); return Some(p); }
1532 "font-black" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("900")); return Some(p); }
1533 _ => {}
1534 }
1535 match class {
1537 "font-sans" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("system-ui, -apple-system, sans-serif")); return Some(p); }
1538 "font-serif" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("Georgia, serif")); return Some(p); }
1539 "font-mono" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("ui-monospace, monospace")); return Some(p); }
1540 _ => {}
1541 }
1542 match class {
1544 "text-xs" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("12px")); p.insert("line-height".into(), json!("16px")); return Some(p); }
1545 "text-sm" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("14px")); p.insert("line-height".into(), json!("20px")); return Some(p); }
1546 "text-base" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("16px")); p.insert("line-height".into(), json!("24px")); return Some(p); }
1547 "text-lg" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("18px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
1548 "text-xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("20px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
1549 "text-2xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("24px")); p.insert("line-height".into(), json!("32px")); return Some(p); }
1550 "text-3xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("30px")); p.insert("line-height".into(), json!("36px")); return Some(p); }
1551 "text-4xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("36px")); p.insert("line-height".into(), json!("40px")); return Some(p); }
1552 "text-5xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("48px")); p.insert("line-height".into(), json!("1")); return Some(p); }
1553 "text-6xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("60px")); p.insert("line-height".into(), json!("1")); return Some(p); }
1554 _ => {}
1555 }
1556 match class {
1558 "text-left" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("left")); return Some(p); }
1559 "text-center" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("center")); return Some(p); }
1560 "text-right" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("right")); return Some(p); }
1561 "text-justify" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("justify")); return Some(p); }
1562 _ => {}
1563 }
1564 match class {
1566 "overflow-auto" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("auto")); return Some(p); }
1567 "overflow-hidden" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("hidden")); return Some(p); }
1568 "overflow-visible" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("visible")); return Some(p); }
1569 "overflow-scroll" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("scroll")); return Some(p); }
1570 "overflow-x-auto" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("auto")); return Some(p); }
1571 "overflow-x-hidden" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("hidden")); return Some(p); }
1572 "overflow-x-scroll" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("scroll")); return Some(p); }
1573 "overflow-y-auto" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("auto")); return Some(p); }
1574 "overflow-y-hidden" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("hidden")); return Some(p); }
1575 "overflow-y-scroll" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("scroll")); return Some(p); }
1576 _ => {}
1577 }
1578 if let Some(value) = class.strip_prefix("opacity-") {
1580 if let Ok(opacity) = value.parse::<f32>() {
1581 let mut p = CssProps::new();
1582 p.insert("opacity".into(), json!(opacity / 100.0));
1583 return Some(p);
1584 }
1585 }
1586 match class {
1588 "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); }
1589 "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); }
1590 "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); }
1591 "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); }
1592 "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); }
1593 "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); }
1594 "shadow-none" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("none")); return Some(p); }
1595 _ => {}
1596 }
1597 if let Some(arb_value) = parse_arbitrary_value(class) {
1599 return Some(arb_value);
1600 }
1601 if let Some(rest) = class.strip_prefix("text-") {
1603 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1604 let mut props = CssProps::new();
1605 props.insert("color".into(), json!(hex));
1606 return Some(props);
1607 }
1608 }
1609 if let Some(rest) = class.strip_prefix("bg-") {
1611 match rest {
1612 "white" => { let mut p = CssProps::new(); p.insert("background-color".into(), json!("#ffffff")); return Some(p); }
1613 "black" => { let mut p = CssProps::new(); p.insert("background-color".into(), json!("#000000")); return Some(p); }
1614 "transparent" => { let mut p = CssProps::new(); p.insert("background-color".into(), json!("#00000000")); return Some(p); }
1615 _ => {}
1616 }
1617 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1618 let mut props = CssProps::new();
1619 props.insert("background-color".into(), json!(hex));
1620 return Some(props);
1621 }
1622 }
1623 if let Some(rest) = class.strip_prefix("divide-") {
1625 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1626 let mut props = CssProps::new();
1627 props.insert("border-color".into(), json!(hex));
1628 return Some(props);
1629 }
1630 }
1631 if class == "border" {
1632 return Some(border_props(None, 1, vars));
1633 }
1634 if let Some(rest) = class.strip_prefix("border-") {
1635 let parts: Vec<&str> = rest.split('-').collect();
1643
1644 let valid_sides = ["t", "b", "l", "r", "x", "y"];
1646 let (side, color_or_width_parts) = if parts.len() > 1 && valid_sides.contains(&parts[0]) {
1647 (Some(parts[0]), &parts[1..])
1648 } else {
1649 (None, &parts[..])
1650 };
1651
1652 if color_or_width_parts.len() == 2 {
1654 let color_shade = color_or_width_parts.join("-");
1656 if let Some(hex) = get_tailwind_color_with_vars(&color_shade, vars) {
1657 let mut props = CssProps::new();
1658 let prop_name = if let Some(s) = side {
1659 format!("border-{}-color", s)
1660 } else {
1661 "border-color".to_string()
1662 };
1663 props.insert(prop_name, json!(hex));
1664 return Some(props);
1665 }
1666 }
1667
1668 if color_or_width_parts.len() == 1 {
1670 let potential_color = format!("{}-500", color_or_width_parts[0]);
1671 if let Some(hex) = get_tailwind_color_with_vars(&potential_color, vars) {
1672 let mut props = CssProps::new();
1673 let prop_name = if let Some(s) = side {
1674 format!("border-{}-color", s)
1675 } else {
1676 "border-color".to_string()
1677 };
1678 props.insert(prop_name, json!(hex));
1679 return Some(props);
1680 }
1681 }
1682
1683 if color_or_width_parts.len() == 1 {
1685 if let Ok(width) = color_or_width_parts[0].parse::<i32>() {
1686 return Some(border_props(side, width, vars));
1687 }
1688 }
1689 }
1690 if class == "rounded" { return Some(rounded_props(None, Some("md"))); }
1692 if let Some(sz) = class.strip_prefix("rounded-") {
1693 return Some(rounded_props(None, Some(sz)));
1694 }
1695 for &(pref, side) in &[("rounded-t", "t"), ("rounded-b", "b"), ("rounded-l", "l"), ("rounded-r", "r")] {
1696 if class == pref { return Some(rounded_props(Some(side), Some("md"))); }
1697 if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
1698 return Some(rounded_props(Some(side), Some(sz)));
1699 }
1700 }
1701 if let Some(cur) = class.strip_prefix("cursor-") {
1703 let mut props = CssProps::new();
1704 props.insert("cursor".into(), json!(match cur {
1705 "pointer" => "pointer",
1706 "default" => "default",
1707 "text" => "text",
1708 "move" => "move",
1709 "wait" => "wait",
1710 "not-allowed" => "not-allowed",
1711 other => other,
1712 }));
1713 return Some(props);
1714 }
1715 if class == "transition" || class == "transition-all" {
1717 let mut props = CssProps::new();
1718 props.insert("transition-property".into(), json!("all"));
1719 props.insert("transition-duration".into(), json!("150ms"));
1720 props.insert("transition-timing-function".into(), json!("ease-in-out"));
1721 return Some(props);
1722 }
1723 if class == "transition-none" {
1724 let mut props = CssProps::new();
1725 props.insert("transition-property".into(), json!("none"));
1726 props.insert("transition-duration".into(), json!("0ms"));
1727 return Some(props);
1728 }
1729 if let Some(rest) = class.strip_prefix("transition-") {
1730 let mut props = CssProps::new();
1732 let property = match rest {
1733 "colors" => "color, background-color, border-color, fill, stroke",
1734 "opacity" => "opacity",
1735 "transform" => "transform",
1736 "shadow" => "box-shadow",
1737 other => other,
1738 };
1739 props.insert("transition-property".into(), json!(property));
1740 props.insert("transition-duration".into(), json!("150ms"));
1741 props.insert("transition-timing-function".into(), json!("ease-in-out"));
1742 return Some(props);
1743 }
1744 if let Some(val) = class.strip_prefix("w-") {
1746 return width_like_props("width", val);
1747 }
1748 if let Some(val) = class.strip_prefix("min-w-") {
1749 return width_like_props("min-width", val);
1750 }
1751 if let Some(val) = class.strip_prefix("max-w-") {
1752 return width_like_props("max-width", val);
1753 }
1754 if let Some(val) = class.strip_prefix("h-") {
1756 return width_like_props("height", val);
1757 }
1758 if let Some(val) = class.strip_prefix("min-h-") {
1759 return width_like_props("min-height", val);
1760 }
1761 if let Some(val) = class.strip_prefix("max-h-") {
1762 return width_like_props("max-height", val);
1763 }
1764 None
1765}
1766
1767fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
1768where
1769 F: Fn(i32) -> CssProps,
1770{
1771 if let Ok(n) = value.parse::<i32>() {
1772 let px = n * 4;
1773 return Some(builder(px));
1774 }
1775 None
1776}
1777
1778fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
1779 let mut props = CssProps::new();
1780 let val = format!("{}px", px_value);
1781 for key in keys {
1782 props.insert((*key).into(), json!(&val));
1783 }
1784 props
1785}
1786
1787fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
1788 let mut props = CssProps::new();
1789 let val = format!("{}px", px_value);
1790 for key in keys {
1791 props.insert((*key).into(), json!(&val));
1792 }
1793 props
1794}
1795
1796fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
1797 let mut props = CssProps::new();
1798 let width_str = format!("{}px", width);
1799 match side {
1800 None => {
1801 props.insert("border-width".into(), json!(&width_str));
1802 }
1803 Some("t") => {
1804 props.insert("border-top-width".into(), json!(&width_str));
1805 }
1806 Some("b") => {
1807 props.insert("border-bottom-width".into(), json!(&width_str));
1808 }
1809 Some("l") => {
1810 props.insert("border-left-width".into(), json!(&width_str));
1811 }
1812 Some("r") => {
1813 props.insert("border-right-width".into(), json!(&width_str));
1814 }
1815 Some("x") => {
1816 props.insert("border-left-width".into(), json!(&width_str));
1817 props.insert("border-right-width".into(), json!(&width_str));
1818 }
1819 Some("y") => {
1820 props.insert("border-top-width".into(), json!(&width_str));
1821 props.insert("border-bottom-width".into(), json!(&width_str));
1822 }
1823 _ => {
1824 props.insert("border-width".into(), json!(&width_str));
1825 }
1826 };
1827 props.insert("border-color".into(), json!("var(border)"));
1828 props.insert("border-style".into(), json!("solid"));
1829 props
1830}
1831
1832fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
1833 let mut props = CssProps::new();
1834 let px = match size.unwrap_or("md") {
1835 "none" => 0,
1836 "sm" => 2,
1837 "md" => 4,
1838 "lg" => 8,
1839 "xl" => 12,
1840 "2xl" => 16,
1841 "3xl" => 24,
1842 "full" => 9999,
1843 s => s.parse::<i32>().unwrap_or(4),
1844 };
1845 let v = json!(format!("{}px", px));
1846 match side {
1847 None => { props.insert("border-radius".into(), v); }
1848 Some("t") => {
1849 props.insert("border-top-left-radius".into(), v.clone());
1850 props.insert("border-top-right-radius".into(), v);
1851 }
1852 Some("b") => {
1853 props.insert("border-bottom-left-radius".into(), v.clone());
1854 props.insert("border-bottom-right-radius".into(), v);
1855 }
1856 Some("l") => { props.insert("border-top-left-radius".into(), v.clone()); props.insert("border-bottom-left-radius".into(), v); }
1857 Some("r") => { props.insert("border-top-right-radius".into(), v.clone()); props.insert("border-bottom-right-radius".into(), v); }
1858 _ => { props.insert("border-radius".into(), v); }
1859 }
1860 props
1861}
1862
1863fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
1864 let mut props = CssProps::new();
1865 let value = match token {
1866 "full" => Some("100%".to_string()),
1867 "screen" => Some(if prop == "width" { "100vw" } else { "100vh" }.to_string()),
1868 "min" => Some("min-content".to_string()),
1869 "max" => Some("max-content".to_string()),
1870 "fit" => Some("fit-content".to_string()),
1871 "auto" => Some("auto".to_string()),
1872 "px" => Some("1px".to_string()),
1873 other => {
1874 if let Some((a, b)) = other.split_once('/') {
1876 if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
1877 let pct = (na / nb) * 100.0;
1878 Some(format!("{}%", trim_trailing_zeros(pct)))
1879 } else { None }
1880 } else if let Ok(n) = other.parse::<i32>() {
1881 Some(format!("{}px", n * 4))
1882 } else {
1883 None
1884 }
1885 }
1886 }?;
1887 props.insert(prop.into(), json!(value));
1888 Some(props)
1889}
1890
1891fn trim_trailing_zeros(num: f64) -> String {
1892 let mut s = format!("{:.6}", num);
1893 while s.contains('.') && s.ends_with('0') { s.pop(); }
1894 if s.ends_with('.') { s.pop(); }
1895 s
1896}
1897
1898fn css_escape_class(class: &str) -> String { class.replace(':', "\\:") }
1903
1904fn class_to_selector(class: &str) -> String {
1905 let (_bp, hover, base) = parse_prefixed_class(class);
1906 if hover {
1907 format!(".{}:hover", css_escape_class(&base))
1908 } else {
1909 format!(".{}", css_escape_class(&base))
1910 }
1911}
1912
1913pub fn post_process_css(
1920 raw_rules: &[(String, CssProps)],
1921 vars: &IndexMap<String, String>,
1922) -> String {
1923 let mut normal = vec![];
1925 let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
1926 for (sel, props) in raw_rules.iter() {
1927 if let Some((media, inner)) = sel.split_once('{') {
1928 if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
1929 let inner_sel = inner.trim_end_matches('}').to_string();
1930 media_map
1931 .entry(media.trim().to_string())
1932 .or_default()
1933 .push((inner_sel, props.clone()));
1934 continue;
1935 }
1936 }
1937 normal.push((sel.clone(), props.clone()));
1938 }
1939 let mut out = String::new();
1940 for (sel, props) in normal {
1941 out.push_str(&sel);
1942 out.push('{');
1943 out.push_str(&css_props_string(&props, vars));
1944 out.push_str("}\n");
1945 }
1946 for (media, entries) in media_map {
1947 out.push_str(&media);
1948 out.push('{');
1949 for (sel, props) in entries {
1950 out.push_str(&sel);
1951 out.push('{');
1952 out.push_str(&css_props_string(&props, vars));
1953 out.push_str("}");
1954 }
1955 out.push_str("}\n");
1956 }
1957 out
1958}
1959
1960fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1963 let parts: Vec<&str> = class.split(':').collect();
1965 if parts.len() == 1 {
1966 return (None, false, class.to_string());
1967 }
1968 let mut bp: Option<String> = None;
1969 let mut hover = false;
1970 for &p in &parts[..parts.len() - 1] {
1971 match p {
1972 "hover" => hover = true,
1973 "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
1974 _ => {}
1975 }
1976 }
1977 let base = parts.last().unwrap().to_string();
1978 (bp, hover, base)
1979}
1980
1981fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
1982 if let Some(k) = bp_key {
1983 if let Some(val) = bps.get(k) {
1984 return format!("@media (min-width: {}) {{{}}}", val, selector);
1985 }
1986 }
1987 selector.to_string()
1988}
1989
1990fn get_tailwind_color(color_shade: &str) -> Option<String> {
1992 let parts: Vec<&str> = color_shade.split('-').collect();
1993 if parts.len() != 2 {
1994 return None;
1995 }
1996 let color_name = parts[0];
1997 let shade = parts[1];
1998
1999 if let Some(hex) = TAILWIND_COLORS
2001 .get(color_name)
2002 .and_then(|shades| shades.get(shade))
2003 {
2004 return Some(hex.to_string());
2005 }
2006
2007 None
2008}
2009
2010fn get_tailwind_color_with_vars(color_shade: &str, vars: &IndexMap<String, String>) -> Option<String> {
2011 if let Some(hex) = get_tailwind_color(color_shade) {
2013 return Some(hex);
2014 }
2015
2016 if let Some(val) = vars.get(color_shade) {
2025 return Some(val.clone());
2026 }
2027
2028 if let Some(val) = vars.get(&format!("colors.{}", color_shade)) {
2030 return Some(val.clone());
2031 }
2032
2033 if let Some(val) = vars.get(&format!("color.{}", color_shade)) {
2035 return Some(val.clone());
2036 }
2037
2038 let parts: Vec<&str> = color_shade.split('-').collect();
2041 if parts.len() >= 1 {
2042 let color_name = parts[0];
2043
2044 if let Some(val) = vars.get(color_name) {
2046 return Some(val.clone());
2047 }
2048
2049 if let Some(val) = vars.get(&format!("color.{}", color_name)) {
2051 return Some(val.clone());
2052 }
2053 }
2054
2055 None
2056}
2057
2058fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
2060 if let Some(bracket_start) = class.find('[') {
2062 if !class.ends_with(']') {
2063 return None;
2064 }
2065 let prefix = &class[..bracket_start];
2066 let value = &class[bracket_start + 1..class.len() - 1];
2067
2068 let mut props = CssProps::new();
2069 match prefix {
2070 "bg" => {
2071 props.insert("background-color".into(), json!(value));
2072 return Some(props);
2073 }
2074 "text" => {
2075 props.insert("color".into(), json!(value));
2076 return Some(props);
2077 }
2078 "border" => {
2079 props.insert("border-color".into(), json!(value));
2080 return Some(props);
2081 }
2082 "divide" => {
2083 props.insert("border-color".into(), json!(value));
2084 return Some(props);
2085 }
2086 _ => return None,
2087 }
2088 }
2089 None
2090}
2091
2092pub mod api {
2094 pub use super::{SelectorStyles, State};
2095}
2096
2097#[cfg(test)]
2098mod tests {
2099 use super::*;
2100
2101 #[test]
2102 fn default_theme_has_p2() {
2103 let mut st = State::new_default();
2104 st.register_tailwind_classes(["p-2".to_string()]);
2105 let css = st.css_for_web();
2106 assert!(css.contains(".p-2{"));
2107 assert!(css.contains("padding:8px"));
2108 }
2109
2110 #[test]
2111 fn android_conversion() {
2112 let mut st = State::new_default();
2113 let mut styles = IndexMap::new();
2115 let mut button_props = IndexMap::new();
2116 button_props.insert("backgroundColor".to_string(), json!("#007bff"));
2117 styles.insert("button".to_string(), button_props);
2118 st.add_theme("default", styles);
2119 st.set_theme("default").ok();
2120
2121 let out = st.android_styles_for("button", &[]);
2122 assert!(out.get("backgroundColor").is_some());
2123 }
2124
2125 #[test]
2126 fn embedded_defaults_and_version() {
2127 let mut st = State::default_state();
2129 st.add_theme("default", IndexMap::new());
2130 st.set_theme("default").ok();
2131
2132 let mut vars = IndexMap::new();
2133 vars.insert("primary".to_string(), "#007bff".to_string());
2134 st.set_variables(vars);
2135
2136 assert!(st.themes.contains_key("default"));
2137 let def = st.themes.get("default").unwrap();
2138 assert!(def.variables.contains_key("primary"));
2139
2140 #[cfg(target_arch = "wasm32")]
2143 {
2144 let v = get_version();
2145 assert!(!v.is_empty());
2146 }
2147 }
2148
2149 #[test]
2150 fn border_color_with_direction() {
2151 let mut st = State::new_default();
2152
2153 st.register_tailwind_classes(["border-b-blue-500".to_string()]);
2155 let css = st.css_for_web();
2156 assert!(css.contains(".border-b-blue-500{"));
2157 assert!(css.contains("border-bottom-color:#3b82f6") || css.contains("border-b-color:#3b82f6"));
2158
2159 st.register_tailwind_classes(["border-t-red-500".to_string()]);
2161 let css = st.css_for_web();
2162 assert!(css.contains(".border-t-red-500{"));
2163
2164 st.register_tailwind_classes(["border-blue-500".to_string()]);
2166 let css = st.css_for_web();
2167 assert!(css.contains(".border-blue-500{"));
2168 assert!(css.contains("border-color:#3b82f6"));
2169 }
2170
2171 #[test]
2172 fn multiple_selectors_support() {
2173 let mut st = State::new_default();
2174 let mut selectors = SelectorStyles::new();
2175 let mut props = CssProps::new();
2176 props.insert("color".to_string(), serde_json::json!("#ff0000"));
2177 selectors.insert("h1, h2, h3".to_string(), props);
2178
2179 st.add_theme("test", selectors);
2180 st.set_theme("test").ok();
2181
2182 let android = st.android_styles_for("h1", &[]);
2184 assert_eq!(android.get("color").and_then(|v| v.as_str()), Some("#ff0000"), "h1 should have red color");
2185
2186 let android = st.android_styles_for("h2", &[]);
2188 assert_eq!(android.get("color").and_then(|v| v.as_str()), Some("#ff0000"), "h2 should have red color");
2189
2190 let android = st.android_styles_for("h3", &[]);
2192 assert_eq!(android.get("color").and_then(|v| v.as_str()), Some("#ff0000"), "h3 should have red color");
2193 }
2194
2195 #[test]
2196 fn multiple_selectors_classes() {
2197 let mut st = State::new_default();
2198 let mut selectors = SelectorStyles::new();
2199 let mut props = CssProps::new();
2200 props.insert("padding".to_string(), serde_json::json!("10px"));
2201 selectors.insert(".btn, .link".to_string(), props);
2202
2203 st.add_theme("test", selectors);
2204 st.set_theme("test").ok();
2205
2206 let android = st.android_styles_for("div", &["btn".to_string()]);
2208 assert_eq!(android.get("padding").and_then(|v| v.as_f64()), Some(10.0), ".btn should have 10px padding");
2209
2210 let android = st.android_styles_for("div", &["link".to_string()]);
2212 assert_eq!(android.get("padding").and_then(|v| v.as_f64()), Some(10.0), ".link should have 10px padding");
2213 }
2214
2215 #[test]
2216 fn border_width_with_direction() {
2217 let mut st = State::new_default();
2218
2219 st.register_tailwind_classes(["border-b-2".to_string()]);
2221 let css = st.css_for_web();
2222 assert!(css.contains(".border-b-2{"));
2223 assert!(css.contains("border-bottom-width:2px"));
2224
2225 st.register_tailwind_classes(["border-2".to_string()]);
2227 let css = st.css_for_web();
2228 assert!(css.contains(".border-2{"));
2229 assert!(css.contains("border-width:2px"));
2230 }
2231
2232 #[test]
2233 fn display_flex_hover_breakpoint() {
2234 let mut st = State::new_default();
2235
2236 st.add_theme("default", IndexMap::new());
2238 st.set_theme("default").ok();
2239
2240 let mut breakpoints = IndexMap::new();
2241 breakpoints.insert("md".to_string(), "768px".to_string());
2242 st.set_breakpoints(breakpoints);
2243
2244 st.register_tailwind_classes([
2245 "block".into(),
2246 "inline-flex".into(),
2247 "hidden".into(),
2248 "md:flex".into(),
2249 "md:hover:block".into(),
2250 ]);
2251 let css = st.css_for_web();
2252 assert!(css.contains(".block{"));
2253 assert!(css.contains("display:block"));
2254 assert!(css.contains(".inline-flex{"));
2255 assert!(css.contains("display:inline-flex"));
2256 assert!(css.contains(".hidden{"));
2257 assert!(css.contains("display:none"));
2258 assert!(css.contains("@media (min-width: 768px)"));
2260 assert!(css.contains(".flex{display:flex"));
2261 assert!(css.contains(":hover{display:block"));
2263
2264 let android = st.android_styles_for("div", &["md:flex".into()]);
2266 assert_eq!(android.get("display").and_then(|v| v.as_str()), Some("flex"));
2267 }
2268
2269 #[test]
2270 fn parse_var_references_basic() {
2271 let refs = parse_var_references("var(color)");
2273 assert_eq!(refs.len(), 1);
2274 assert_eq!(refs[0].2, "color");
2275 assert_eq!(refs[0].0, 0); assert_eq!(refs[0].1, 10); let refs = parse_var_references("var(--primary)");
2280 assert_eq!(refs.len(), 1);
2281 assert_eq!(refs[0].2, "primary");
2282
2283 let refs = parse_var_references("var(--color) and var(size)");
2285 assert_eq!(refs.len(), 2);
2286 assert_eq!(refs[0].2, "color");
2287 assert_eq!(refs[1].2, "size");
2288
2289 let refs = parse_var_references("var( --spacing )");
2291 assert_eq!(refs.len(), 1);
2292 assert_eq!(refs[0].2, "spacing");
2293
2294 let refs = parse_var_references("var(color.primary-500)");
2296 assert_eq!(refs.len(), 1);
2297 assert_eq!(refs[0].2, "color.primary-500");
2298
2299 let refs = parse_var_references("no variables here");
2301 assert_eq!(refs.len(), 0);
2302
2303 let refs = parse_var_references("var(");
2305 assert_eq!(refs.len(), 0);
2306
2307 let refs = parse_var_references("var(color");
2309 assert_eq!(refs.len(), 0);
2310 }
2311
2312 #[test]
2313 fn resolve_vars_basic() {
2314 let mut vars = IndexMap::new();
2315 vars.insert("primary".to_string(), "#ff0000".to_string());
2316 vars.insert("spacing".to_string(), "8px".to_string());
2317 vars.insert("color.blue".to_string(), "#0000ff".to_string());
2318
2319 assert_eq!(resolve_vars("var(--primary)", &vars), "#ff0000");
2321 assert_eq!(resolve_vars("var(primary)", &vars), "#ff0000");
2322 assert_eq!(resolve_vars("var( --primary )", &vars), "#ff0000");
2323
2324 assert_eq!(
2326 resolve_vars("var(--primary) var(--spacing)", &vars),
2327 "#ff0000 8px"
2328 );
2329
2330 assert_eq!(resolve_vars("var(--color.blue)", &vars), "#0000ff");
2332
2333 assert_eq!(resolve_vars("var(--undefined)", &vars), "var(--undefined)");
2335
2336 assert_eq!(resolve_vars("$primary", &vars), "#ff0000");
2338
2339 assert_eq!(resolve_vars("plain text", &vars), "plain text");
2341 }
2342
2343 #[test]
2344 fn resolve_vars_edge_cases() {
2345 let mut vars = IndexMap::new();
2346 vars.insert("a".to_string(), "1".to_string());
2347 vars.insert("b".to_string(), "2".to_string());
2348
2349 assert_eq!(resolve_vars("var(a)var(b)", &vars), "12");
2351
2352 assert_eq!(resolve_vars("prefix var(a) suffix", &vars), "prefix 1 suffix");
2354
2355 assert_eq!(resolve_vars("", &vars), "");
2357
2358 vars.insert("var123".to_string(), "value".to_string());
2360 assert_eq!(resolve_vars("var(var123)", &vars), "value");
2361
2362 vars.insert("my_var".to_string(), "test".to_string());
2364 assert_eq!(resolve_vars("var(my_var)", &vars), "test");
2365 }
2366
2367 #[test]
2368 fn test_android_scrolling_mapping() {
2369 let mut state = State::default();
2370 state.display_density = 2.0;
2371 state.scaled_density = 2.0;
2372 state.current_theme = "default".to_string();
2373
2374 let mut themes = IndexMap::new();
2375 let mut default_theme = crate::ThemeEntry::default();
2376 default_theme.name = Some("Default".to_string());
2377
2378 let mut overflow_styles = IndexMap::new();
2379 overflow_styles.insert("overflowX".to_string(), serde_json::json!("auto"));
2380 overflow_styles.insert("overflowY".to_string(), serde_json::json!("scroll"));
2381
2382 default_theme.selectors.insert(".scroller".to_string(), overflow_styles);
2383 themes.insert("default".to_string(), default_theme);
2384 state.themes = themes;
2385
2386 let styles = state.android_styles_for("div", &vec![".scroller".to_string()]);
2387
2388 assert_eq!(styles.get("androidScrollHorizontal"), Some(&serde_json::json!(true)));
2389 assert_eq!(styles.get("androidScrollVertical"), Some(&serde_json::json!(true)));
2390 }
2391
2392 #[test]
2393 fn android_flex_row_default() {
2394 let st = State::new_default();
2395 let styles = st.android_styles_for("div", &["flex".to_string()]);
2397 assert_eq!(styles.get("androidOrientation").and_then(|v| v.as_str()), Some("horizontal"));
2398 assert_eq!(styles.get("flexDirection").and_then(|v| v.as_str()), Some("row"));
2399
2400 let styles = st.android_styles_for("div", &[]);
2402 assert_eq!(styles.get("androidOrientation").and_then(|v| v.as_str()), Some("vertical"));
2403 assert_eq!(styles.get("flexDirection").and_then(|v| v.as_str()), Some("column"));
2404 }
2405
2406 #[test]
2407 fn android_gap_orientation_order() {
2408 let st = State::new_default();
2409 let styles = st.android_styles_for("div", &["flex".to_string(), "gap-4".to_string()]);
2410
2411 let keys: Vec<&String> = styles.keys().collect();
2413 let orientation_idx = keys.iter().position(|&k| k == "androidOrientation").unwrap();
2414 let gap_idx = keys.iter().position(|&k| k == "gap").unwrap();
2415
2416 assert!(orientation_idx < gap_idx, "androidOrientation should come before gap for correct layout processing");
2417 }
2418
2419 #[test]
2420 fn margin_auto_support() {
2421 let mut st = State::new_default();
2422 st.register_tailwind_classes(["ml-auto".to_string(), "mr-auto".to_string(), "mx-auto".to_string()]);
2423
2424 let css = st.css_for_web();
2426 assert!(css.contains("margin-left:auto"));
2427 assert!(css.contains("margin-right:auto"));
2428
2429 let styles = st.android_styles_for("div", &["ml-auto".to_string()]);
2431 assert_eq!(styles.get("marginLeft").and_then(|v| v.as_str()), Some("auto"));
2432
2433 let styles = st.android_styles_for("div", &["mx-auto".to_string()]);
2434 assert_eq!(styles.get("marginLeft").and_then(|v| v.as_str()), Some("auto"));
2435 assert_eq!(styles.get("marginRight").and_then(|v| v.as_str()), Some("auto"));
2436 }
2437
2438 #[test]
2439 fn alignment_mapping() {
2440 let st = State::new_default();
2441
2442 let row_styles = st.android_styles_for("div", &["flex".to_string(), "justify-center".to_string(), "items-center".to_string()]);
2444 assert_eq!(row_styles.get("androidOrientation").and_then(|v| v.as_str()), Some("horizontal"));
2445 assert_eq!(row_styles.get("androidGravity").and_then(|v| v.as_str()), Some("center"));
2447
2448 let col_styles = st.android_styles_for("div", &["flex".to_string(), "flex-col".to_string(), "justify-center".to_string(), "items-center".to_string()]);
2450 assert_eq!(col_styles.get("androidOrientation").and_then(|v| v.as_str()), Some("vertical"));
2451 assert_eq!(col_styles.get("androidGravity").and_then(|v| v.as_str()), Some("center"));
2453
2454 let row_start_styles = st.android_styles_for("div", &["flex".to_string(), "justify-start".to_string(), "items-end".to_string()]);
2456 assert_eq!(row_start_styles.get("androidGravity").and_then(|v| v.as_str()), Some("bottom|start"));
2457 }
2458
2459 #[test]
2460 fn test_button_bg_override() {
2461 let mut themes = IndexMap::new();
2462
2463 let mut variables = IndexMap::new();
2464 variables.insert("color.bg".to_string(), "#ffffff".to_string());
2465
2466 let mut selectors = IndexMap::new();
2467 let mut button_props = IndexMap::new();
2468 button_props.insert("background-color".to_string(), json!("#2563eb"));
2469 selectors.insert("button".to_string(), button_props);
2470
2471 let default_theme = ThemeEntry {
2472 name: Some("default".to_string()),
2473 inherits: None,
2474 selectors,
2475 variables,
2476 breakpoints: IndexMap::new(),
2477 };
2478
2479 themes.insert("default".to_string(), default_theme);
2480
2481 let mut state = State::new_default();
2482 state.themes = themes;
2483 state.current_theme = "default".to_string();
2484
2485 let classes = vec!["bg-bg".to_string(), "p-4".to_string()];
2487 let styles = state.android_styles_for("button", &classes);
2488
2489 println!("[test_button_bg_override] styles: {:?}", styles);
2490
2491 assert_eq!(styles.get("backgroundColor").and_then(|v: &serde_json::Value| v.as_str()), Some("#ffffff"));
2493
2494 assert_eq!(styles.get("paddingTop"), Some(&serde_json::json!(16)));
2497 assert_eq!(styles.get("paddingVertical"), Some(&serde_json::json!(16)));
2498 }
2499}
2500
2501#[cfg(all(target_os = "android", feature = "android"))]
2502#[cfg(feature = "android")]
2503mod android_jni;
2504
2505mod bridge_common;
2506mod ffi;
2507
2508pub use ffi::*;
2509
2510#[cfg(target_vendor = "apple")]
2511mod ios_ffi;