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