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 color;
8mod default_state;
9use default_state::bundled_state;
10
11fn default_display_density() -> f32 {
13 1.0
14}
15fn default_scaled_density() -> f32 {
16 1.0
17}
18
19pub type CssProps = IndexMap<String, serde_json::Value>;
20pub type SelectorStyles = IndexMap<String, CssProps>; fn dp_to_px(dp: f32, density: f32) -> i32 {
24 (dp * density).round() as i32
25}
26
27fn sp_to_px(sp: f32, scaled_density: f32) -> f32 {
29 sp * scaled_density
30}
31
32fn parse_and_convert_to_px(value: &serde_json::Value, density: f32) -> Option<serde_json::Value> {
34 match value {
35 serde_json::Value::Number(n) => {
36 let dp = n.as_f64()? as f32;
38 Some(serde_json::json!(dp_to_px(dp, density)))
39 }
40 serde_json::Value::String(s) => {
41 let trimmed = s.trim();
43 if trimmed.ends_with("px") {
44 let px = trimmed.trim_end_matches("px").trim().parse::<f32>().ok()?;
46 Some(serde_json::json!(dp_to_px(px, density)))
47 } else if trimmed.ends_with("dp") {
48 let dp = trimmed.trim_end_matches("dp").trim().parse::<f32>().ok()?;
49 Some(serde_json::json!(dp_to_px(dp, density)))
50 } else if trimmed.ends_with("rem") {
51 let rem = trimmed.trim_end_matches("rem").trim().parse::<f32>().ok()?;
53 let dp = rem * 16.0;
54 Some(serde_json::json!(dp_to_px(dp, density)))
55 } else if trimmed.ends_with("em") {
56 let em = trimmed.trim_end_matches("em").trim().parse::<f32>().ok()?;
58 let dp = em * 16.0;
59 Some(serde_json::json!(dp_to_px(dp, density)))
60 } else if let Ok(num) = trimmed.parse::<f32>() {
61 Some(serde_json::json!(dp_to_px(num, density)))
63 } else {
64 None
66 }
67 }
68 _ => None,
69 }
70}
71
72fn deserialize_variables<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
73where
74 D: Deserializer<'de>,
75{
76 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
77 let mut out: IndexMap<String, String> = IndexMap::new();
78 if let Some(v) = value {
79 flatten_variables(None, &v, &mut out);
80 }
81 Ok(out)
82}
83
84fn flatten_variables(
85 prefix: Option<&str>,
86 value: &serde_json::Value,
87 out: &mut IndexMap<String, String>,
88) {
89 match value {
90 serde_json::Value::Object(map) => {
91 for (k, v) in map {
92 let key = if let Some(p) = prefix {
93 format!("{}.{}", p, k)
94 } else {
95 k.to_string()
96 };
97 flatten_variables(Some(&key), v, out);
98 }
99 }
100 serde_json::Value::Array(arr) => {
101 for (idx, v) in arr.iter().enumerate() {
102 let key = if let Some(p) = prefix {
103 format!("{}.{}", p, idx)
104 } else {
105 idx.to_string()
106 };
107 flatten_variables(Some(&key), v, out);
108 }
109 }
110 serde_json::Value::Null => {}
111 serde_json::Value::Bool(b) => {
112 if let Some(p) = prefix {
113 out.insert(p.to_string(), b.to_string());
114 }
115 }
116 serde_json::Value::Number(n) => {
117 if let Some(p) = prefix {
118 out.insert(p.to_string(), n.to_string());
119 }
120 }
121 serde_json::Value::String(s) => {
122 if let Some(p) = prefix {
123 out.insert(p.to_string(), s.clone());
124 }
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, Default)]
130pub struct ThemeEntry {
131 #[serde(default)]
132 pub name: Option<String>,
133 #[serde(default)]
134 pub inherits: Option<String>,
135 #[serde(default, rename = "inheritsDark", alias = "inherits_dark")]
136 pub inherits_dark: Option<String>,
137 #[serde(default)]
138 pub selectors: SelectorStyles,
139 #[serde(default, deserialize_with = "deserialize_variables")]
140 pub variables: IndexMap<String, String>,
141 #[serde(default, deserialize_with = "deserialize_variables")]
142 pub breakpoints: IndexMap<String, String>,
143}
144
145impl ThemeEntry {
146 fn parent_name(&self, prefers_dark: bool) -> Option<String> {
147 if prefers_dark {
148 self.inherits_dark.clone().or_else(|| self.inherits.clone())
149 } else {
150 self.inherits.clone()
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, Default)]
156pub struct State {
157 pub themes: IndexMap<String, ThemeEntry>,
159 pub default_theme: String,
160 pub current_theme: String,
161 #[serde(default)]
162 pub prefers_color_scheme: Option<String>,
163 #[serde(default = "default_display_density")]
165 pub display_density: f32, #[serde(default = "default_scaled_density")]
167 pub scaled_density: f32, #[serde(default)]
170 pub used_classes: IndexSet<String>, #[serde(default)]
172 pub used_tags: IndexSet<String>, #[serde(default)]
175 pub used_tag_classes: IndexSet<String>,
176}
177
178#[derive(thiserror::Error, Debug)]
179pub enum Error {
180 #[error("theme not found: {0}")]
181 ThemeNotFound(String),
182}
183
184impl State {
185 pub fn new_default() -> Self {
186 return bundled_state();
188 }
189
190 pub fn default_state() -> Self {
192 bundled_state()
193 }
194
195 pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
196 let name = theme.into();
197 if !self.themes.contains_key(&name) {
198 return Err(Error::ThemeNotFound(name));
199 }
200 self.current_theme = name;
201 Ok(())
202 }
203
204 pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
205 let name = name.into();
206 let entry = self.themes.entry(name).or_default();
207 for (sel, props) in styles.into_iter() {
208 let e = entry.selectors.entry(sel).or_default();
209 merge_props(e, &props);
210 }
211 }
212
213 pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
214 let cur = self.current_theme.clone();
216 let entry = self.themes.entry(cur).or_default();
217 entry.variables = vars;
218 }
219
220 pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
221 let cur = self.current_theme.clone();
222 let entry = self.themes.entry(cur).or_default();
223 entry.breakpoints = map;
224 }
225
226 pub fn process_styles(
227 &self,
228 mut styles: IndexMap<String, serde_json::Value>,
229 ) -> IndexMap<String, serde_json::Value> {
230 let density = self.display_density;
231
232 if let Some(ph) = styles.get("paddingHorizontal").cloned() {
236 styles.entry("paddingLeft".into()).or_insert(ph.clone());
237 styles.entry("paddingRight".into()).or_insert(ph.clone());
238 }
239 if let Some(pv) = styles.get("paddingVertical").cloned() {
240 styles.entry("paddingTop".into()).or_insert(pv.clone());
241 styles.entry("paddingBottom".into()).or_insert(pv.clone());
242 }
243 if let Some(p) = styles.get("padding").cloned() {
244 styles.entry("paddingTop".into()).or_insert(p.clone());
245 styles.entry("paddingBottom".into()).or_insert(p.clone());
246 styles.entry("paddingLeft".into()).or_insert(p.clone());
247 styles.entry("paddingRight".into()).or_insert(p.clone());
248 }
249 if let Some(mh) = styles.get("marginHorizontal").cloned() {
250 styles.entry("marginLeft".into()).or_insert(mh.clone());
251 styles.entry("marginRight".into()).or_insert(mh.clone());
252 }
253 if let Some(mv) = styles.get("marginVertical").cloned() {
254 styles.entry("marginTop".into()).or_insert(mv.clone());
255 styles.entry("marginBottom".into()).or_insert(mv.clone());
256 }
257 if let Some(m) = styles.get("margin").cloned() {
258 styles.entry("marginTop".into()).or_insert(m.clone());
259 styles.entry("marginBottom".into()).or_insert(m.clone());
260 styles.entry("marginLeft".into()).or_insert(m.clone());
261 styles.entry("marginRight".into()).or_insert(m.clone());
262 }
263 if let Some(r) = styles.get("borderRadius").cloned() {
264 styles
265 .entry("borderTopLeftRadius".into())
266 .or_insert(r.clone());
267 styles
268 .entry("borderTopRightRadius".into())
269 .or_insert(r.clone());
270 styles
271 .entry("borderBottomLeftRadius".into())
272 .or_insert(r.clone());
273 styles
274 .entry("borderBottomRightRadius".into())
275 .or_insert(r.clone());
276 }
277
278 let dimension_props = [
280 "width",
281 "height",
282 "minWidth",
283 "minHeight",
284 "maxWidth",
285 "maxHeight",
286 "padding",
287 "paddingTop",
288 "paddingBottom",
289 "paddingLeft",
290 "paddingRight",
291 "paddingHorizontal",
292 "paddingVertical",
293 "margin",
294 "marginTop",
295 "marginBottom",
296 "marginLeft",
297 "marginRight",
298 "marginHorizontal",
299 "marginVertical",
300 "borderRadius",
301 "borderTopLeftRadius",
302 "borderTopRightRadius",
303 "borderBottomLeftRadius",
304 "borderBottomRightRadius",
305 "borderWidth",
306 "borderTopWidth",
307 "borderBottomWidth",
308 "borderLeftWidth",
309 "borderRightWidth",
310 "gap",
311 "rowGap",
312 "columnGap",
313 "elevation",
314 "fontSize",
315 "lineHeight",
316 "letterSpacing",
317 ];
318
319 for prop in &dimension_props {
320 if let Some(value) = styles.get(*prop).cloned() {
321 if let Some(converted) = parse_and_convert_to_px(&value, density) {
322 styles.insert(prop.to_string(), converted);
323 }
324 }
325 }
326
327 styles
328 }
329
330 pub fn set_default_theme(&mut self, name: impl Into<String>) {
331 self.default_theme = name.into();
332 }
333
334 pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, _selectors: I) {
335 }
337
338 pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
339 for c in classes {
340 self.used_classes.insert(c);
341 }
342 }
343
344 pub fn register_tags<I: IntoIterator<Item = String>>(&mut self, tags: I) {
345 for t in tags {
346 self.used_tags.insert(t);
347 }
348 }
349
350 pub fn register_tag_class(&mut self, tag: impl Into<String>, class_: impl Into<String>) {
351 let key = format!("{}|{}", tag.into(), class_.into());
352 self.used_tag_classes.insert(key);
353 }
354
355 pub fn clear_usage(&mut self) {
356 self.used_classes.clear();
357 self.used_tags.clear();
358 self.used_tag_classes.clear();
359 }
360
361 pub fn to_json(&self) -> serde_json::Value {
362 json!({
363 "themes": self.themes,
364 "default_theme": self.default_theme,
365 "current_theme": self.current_theme,
366 "display_density": self.display_density,
367 "scaled_density": self.scaled_density,
368 "used_classes": self.used_classes,
369 "used_tags": self.used_tags,
370 "used_tag_classes": self.used_tag_classes,
371 })
372 }
373
374 pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
375 let state: State = serde_json::from_value(value)?;
376 Ok(state)
377 }
378
379 pub fn css_for_web(&self) -> String {
380 let (eff, vars) = self.effective_theme_all();
382 let bps = self.effective_breakpoints();
383 let mut rules: Vec<(String, CssProps)> = Vec::new();
384
385 let mut used_tags: IndexSet<String> = self.used_tags.clone();
387 let mut used_classes: IndexSet<String> = self.used_classes.clone();
388 for key in &self.used_tag_classes {
389 if let Some((t, c)) = split_tag_class_key(key) {
390 used_tags.insert(t);
391 used_classes.insert(c);
392 }
393 }
394
395 for (sel, props) in eff.iter() {
401 if should_emit_selector(sel, &used_tags, &used_classes, &self.used_tag_classes) {
402 rules.push((sel.clone(), props.clone()));
403 }
404 }
405
406 for class in &used_classes {
408 let (bp_key, hover, base) = parse_prefixed_class(class);
409 let selector = if hover {
410 format!(".{}:hover", css_escape_class(&base))
411 } else {
412 format!(".{}", css_escape_class(&base))
413 };
414
415 if eff.get(&selector).is_some() {
418 continue;
419 }
420 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
422 let sel = if hover {
423 format!(".{}:hover", css_escape_class(&base))
424 } else {
425 format!(".{}", css_escape_class(&base))
426 };
427 let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
428 rules.push((final_sel, dynamic_props));
429 continue;
430 }
431 if let Some(props) = eff.get(&base) {
433 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
434 rules.push((final_sel, props.clone()));
435 }
436 }
437
438 post_process_css(&rules, &vars)
439 }
440
441 pub fn android_base_styles(
442 &self,
443 selector: &str,
444 classes: &[String],
445 ) -> IndexMap<String, serde_json::Value> {
446 let (eff, vars) = self.effective_theme_all();
447 let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
448
449 out.insert(
451 "androidOrientation".to_string(),
452 serde_json::json!("vertical"),
453 );
454
455 let mut combined_props = CssProps::new();
456
457 match selector.to_lowercase().as_str() {
459 "div" => {
460 combined_props.insert("width".into(), json!("match_parent"));
461 }
462 "p" => {
463 combined_props.insert("width".into(), json!("match_parent"));
464 combined_props.insert("margin-vertical".into(), json!("16px"));
465 }
466 "h1" => {
467 combined_props.insert("width".into(), json!("match_parent"));
468 combined_props.insert("font-size".into(), json!("32px"));
469 combined_props.insert("font-weight".into(), json!("bold"));
470 combined_props.insert("margin-vertical".into(), json!("21.44px"));
471 }
472 "h2" => {
473 combined_props.insert("width".into(), json!("match_parent"));
474 combined_props.insert("font-size".into(), json!("24px"));
475 combined_props.insert("font-weight".into(), json!("bold"));
476 combined_props.insert("margin-vertical".into(), json!("19.92px"));
477 }
478 "h3" => {
479 combined_props.insert("width".into(), json!("match_parent"));
480 combined_props.insert("font-size".into(), json!("18.72px"));
481 combined_props.insert("font-weight".into(), json!("bold"));
482 combined_props.insert("margin-vertical".into(), json!("18.72px"));
483 }
484 "h4" => {
485 combined_props.insert("width".into(), json!("match_parent"));
486 combined_props.insert("font-size".into(), json!("16px"));
487 combined_props.insert("font-weight".into(), json!("bold"));
488 combined_props.insert("margin-vertical".into(), json!("21.28px"));
489 }
490 "h5" => {
491 combined_props.insert("width".into(), json!("match_parent"));
492 combined_props.insert("font-size".into(), json!("13.28px"));
493 combined_props.insert("font-weight".into(), json!("bold"));
494 combined_props.insert("margin-vertical".into(), json!("22.17px"));
495 }
496 "h6" => {
497 combined_props.insert("width".into(), json!("match_parent"));
498 combined_props.insert("font-size".into(), json!("10.72px"));
499 combined_props.insert("font-weight".into(), json!("bold"));
500 combined_props.insert("margin-vertical".into(), json!("24.96px"));
501 }
502 "input" => {
503 combined_props.insert("padding-vertical".into(), json!("8px"));
504 combined_props.insert("padding-horizontal".into(), json!("12px"));
505 combined_props.insert("border-radius".into(), json!("4px"));
506 combined_props.insert("border-width".into(), json!("1px"));
507 combined_props.insert("border-color".into(), json!("#cccccc"));
508 combined_props.insert("background-color".into(), json!("#ffffff"));
509 combined_props.insert("color".into(), json!("#000000"));
510 combined_props.insert("placeholder-color".into(), json!("#88888870"));
511 combined_props.insert("min-height".into(), json!("40px"));
512 combined_props.insert("android-gravity".into(), json!("center_vertical"));
513 }
514 "select" => {
515 combined_props.insert("padding-vertical".into(), json!("8px"));
516 combined_props.insert("padding-horizontal".into(), json!("12px"));
517 combined_props.insert("border-radius".into(), json!("4px"));
518 combined_props.insert("border-width".into(), json!("1px"));
519 combined_props.insert("border-color".into(), json!("#cccccc"));
520 combined_props.insert("background-color".into(), json!("#ffffff"));
521 combined_props.insert("color".into(), json!("#000000"));
522 combined_props.insert("min-height".into(), json!("40px"));
523 combined_props.insert("android-gravity".into(), json!("center_vertical"));
524 }
525 "textarea" => {
526 combined_props.insert("padding".into(), json!("12px"));
527 combined_props.insert("border-radius".into(), json!("4px"));
528 combined_props.insert("border-width".into(), json!("1px"));
529 combined_props.insert("border-color".into(), json!("#cccccc"));
530 combined_props.insert("background-color".into(), json!("#ffffff"));
531 combined_props.insert("color".into(), json!("#000000"));
532 combined_props.insert(
533 "placeholder-color".into(),
534 json!("color-mix(in srgb, currentColor 75%, grey)"),
535 );
536 combined_props.insert("min-height".into(), json!("80px"));
537 combined_props.insert("android-gravity".into(), json!("top"));
538 }
539 "button" => {
540 combined_props.insert("padding-vertical".into(), json!("8px"));
541 combined_props.insert("padding-horizontal".into(), json!("16px"));
542 combined_props.insert("border-radius".into(), json!("4px"));
543 combined_props.insert("background-color".into(), json!("#2196F3"));
544 combined_props.insert("color".into(), json!("#ffffff"));
545 combined_props.insert("android-gravity".into(), json!("center"));
546 }
547 _ => {}
548 }
549
550 if selector == "button"
551 || selector == "input"
552 || selector == "textarea"
553 || classes.iter().any(|c| c.contains("bg-"))
554 {
555 log::debug!(
556 "[android_base_styles] selector={} classes={:?}",
557 selector,
558 classes
559 );
560 }
561
562 if let Some(props) = eff.get(selector) {
564 merge_props(&mut combined_props, props);
565 }
566
567 for class in classes {
569 let normalized_class = if class.starts_with('.') {
571 class[1..].to_string()
572 } else {
573 class.clone()
574 };
575
576 let (_bp, _hover, base) = parse_prefixed_class(&normalized_class);
577 let sel = class_to_selector(&base);
579 if let Some(props) = eff.get(&sel) {
580 merge_props(&mut combined_props, props);
581 continue;
582 }
583 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
585 merge_props(&mut combined_props, &dynamic_props);
586 continue;
587 }
588 if let Some(props) = eff.get(&base) {
589 merge_props(&mut combined_props, props);
590 }
591 }
592
593 if selector == "input" || selector == "textarea" {
594 log::debug!(
595 "[android_base_styles] combined_props for {}: {:?}",
596 selector,
597 combined_props
598 );
599 }
600
601 merge_android_props(&mut out, &combined_props, &vars);
602
603 if let Some(display) = out.get("display") {
605 if display.as_str() == Some("flex") && !out.contains_key("flexDirection") {
606 out.insert("flexDirection".to_string(), serde_json::json!("row"));
607 out.insert(
608 "androidOrientation".to_string(),
609 serde_json::json!("horizontal"),
610 );
611 }
612 }
613
614 if !out.contains_key("flexDirection") {
616 match selector.to_lowercase().as_str() {
617 "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" => {
618 out.insert("flexDirection".to_string(), serde_json::json!("column"));
619 out.insert(
620 "androidOrientation".to_string(),
621 serde_json::json!("vertical"),
622 );
623 }
624 _ => {}
625 }
626 }
627
628 if let Some(fd) = out.get("flexDirection").and_then(|v| v.as_str()) {
630 if !out.contains_key("androidOrientation") {
631 let orientation = if fd == "column" || fd == "column-reverse" {
632 "vertical"
633 } else {
634 "horizontal"
635 };
636 out.insert(
637 "androidOrientation".to_string(),
638 serde_json::json!(orientation),
639 );
640 }
641 }
642
643 out
644 }
645
646 pub fn web_styles_for(
649 &self,
650 selector: &str,
651 classes: &[String],
652 ) -> IndexMap<String, serde_json::Value> {
653 let (eff, vars) = self.effective_theme_all();
654 let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
655 let mut combined_props = CssProps::new();
656
657 match selector.to_lowercase().as_str() {
659 "div" => {
660 combined_props.insert("width".into(), json!("100%"));
661 }
662 "p" => {
663 combined_props.insert("width".into(), json!("100%"));
664 combined_props.insert("margin-vertical".into(), json!("16px"));
665 }
666 "h1" => {
667 combined_props.insert("width".into(), json!("100%"));
668 combined_props.insert("font-size".into(), json!("32px"));
669 combined_props.insert("font-weight".into(), json!("bold"));
670 combined_props.insert("margin-vertical".into(), json!("21.44px"));
671 }
672 "h2" => {
673 combined_props.insert("width".into(), json!("100%"));
674 combined_props.insert("font-size".into(), json!("24px"));
675 combined_props.insert("font-weight".into(), json!("bold"));
676 combined_props.insert("margin-vertical".into(), json!("19.92px"));
677 }
678 "h3" => {
679 combined_props.insert("width".into(), json!("100%"));
680 combined_props.insert("font-size".into(), json!("18.72px"));
681 combined_props.insert("font-weight".into(), json!("bold"));
682 combined_props.insert("margin-vertical".into(), json!("18.72px"));
683 }
684 "h4" => {
685 combined_props.insert("width".into(), json!("100%"));
686 combined_props.insert("font-size".into(), json!("16px"));
687 combined_props.insert("font-weight".into(), json!("bold"));
688 combined_props.insert("margin-vertical".into(), json!("21.28px"));
689 }
690 "h5" => {
691 combined_props.insert("width".into(), json!("100%"));
692 combined_props.insert("font-size".into(), json!("13.28px"));
693 combined_props.insert("font-weight".into(), json!("bold"));
694 combined_props.insert("margin-vertical".into(), json!("22.17px"));
695 }
696 "h6" => {
697 combined_props.insert("width".into(), json!("100%"));
698 combined_props.insert("font-size".into(), json!("10.72px"));
699 combined_props.insert("font-weight".into(), json!("bold"));
700 combined_props.insert("margin-vertical".into(), json!("24.96px"));
701 }
702 "input" | "select" | "textarea" | "button" => {
703 combined_props.insert("width".into(), json!("100%"));
704 }
705 _ => {}
706 }
707
708 if let Some(props) = eff.get(selector) {
710 merge_props(&mut combined_props, props);
711 }
712
713 for class in classes {
715 let normalized_class = if class.starts_with('.') {
716 class[1..].to_string()
717 } else {
718 class.clone()
719 };
720 let (_bp, _hover, base) = parse_prefixed_class(&normalized_class);
721 let sel = class_to_selector(&base);
722 if let Some(props) = eff.get(&sel) {
723 merge_props(&mut combined_props, props);
724 continue;
725 }
726 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
727 merge_props(&mut combined_props, &dynamic_props);
728 continue;
729 }
730 if let Some(props) = eff.get(&base) {
731 merge_props(&mut combined_props, props);
732 }
733 }
734
735 merge_web_props(&mut out, &combined_props, &vars);
736
737 if let Some(display) = out.get("display") {
739 if display.as_str() == Some("flex") && !out.contains_key("flexDirection") {
740 out.insert("flexDirection".to_string(), serde_json::json!("row"));
741 }
742 }
743 if !out.contains_key("flexDirection") {
744 match selector.to_lowercase().as_str() {
745 "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" => {
746 out.insert("flexDirection".to_string(), serde_json::json!("column"));
747 }
748 _ => {}
749 }
750 }
751
752 out
753 }
754
755 pub fn android_styles_for(
759 &self,
760 selector: &str,
761 classes: &[String],
762 ) -> IndexMap<String, serde_json::Value> {
763 let mut styles = self.android_base_styles(selector, classes);
764
765 let density = self.display_density;
766 let scaled_density = self.scaled_density;
767
768 if let Some(flex_dir) = styles.get("flexDirection") {
770 let orientation = if flex_dir.as_str() == Some("row") {
771 "horizontal"
772 } else {
773 "vertical"
774 };
775 styles.shift_insert(
776 0,
777 "androidOrientation".to_string(),
778 serde_json::json!(orientation),
779 );
780 }
781
782 let dimension_props = [
784 "width",
785 "height",
786 "minWidth",
787 "minHeight",
788 "maxWidth",
789 "maxHeight",
790 "padding",
791 "paddingTop",
792 "paddingBottom",
793 "paddingLeft",
794 "paddingRight",
795 "paddingHorizontal",
796 "paddingVertical",
797 "margin",
798 "marginTop",
799 "marginBottom",
800 "marginLeft",
801 "marginRight",
802 "marginHorizontal",
803 "marginVertical",
804 "borderRadius",
805 "borderWidth",
806 "borderTopWidth",
807 "borderBottomWidth",
808 "borderLeftWidth",
809 "borderRightWidth",
810 "gap",
811 "rowGap",
812 "columnGap",
813 "spaceX",
814 "spaceY",
815 "elevation",
816 "lineHeight",
817 "letterSpacing",
818 ];
819
820 for prop in &dimension_props {
821 if let Some(value) = styles.get(*prop).cloned() {
822 if let Some(converted) = parse_and_convert_to_px(&value, density) {
823 styles.insert(prop.to_string(), converted);
824 }
825 }
826 }
827
828 if let Some(font_size) = styles.get("fontSize").cloned() {
830 if let Some(serde_json::Value::Number(n)) =
831 parse_and_convert_to_px(&font_size, density).as_ref()
832 {
833 let sp_value = n.as_f64().unwrap_or(14.0) as f32 / density;
835 styles.insert("fontSize".to_string(), serde_json::json!(sp_value));
836 }
837 }
838
839 if let Some(flex_wrap) = styles.get("flexWrap") {
841 if flex_wrap.as_str() == Some("wrap") {
842 styles.insert("androidFlexWrap".to_string(), serde_json::json!(true));
843 }
844 }
845
846 if let Some(opacity) = styles.get("opacity").cloned() {
848 styles.insert("androidAlpha".to_string(), opacity);
849 }
850
851 let is_horizontal =
852 styles.get("androidOrientation").and_then(|v| v.as_str()) == Some("horizontal");
853 let mut gravity_parts = Vec::new();
854
855 if let Some(align_items) = styles.get("alignItems") {
857 let part = match align_items.as_str() {
858 Some("center") => {
859 if is_horizontal {
860 "center_vertical"
861 } else {
862 "center_horizontal"
863 }
864 }
865 Some("flex-start") | Some("start") => {
866 if is_horizontal {
867 "top"
868 } else {
869 "start"
870 }
871 }
872 Some("flex-end") | Some("end") => {
873 if is_horizontal {
874 "bottom"
875 } else {
876 "end"
877 }
878 }
879 Some("stretch") => {
880 if is_horizontal {
881 "fill_vertical"
882 } else {
883 "fill_horizontal"
884 }
885 }
886 _ => "",
887 };
888 if !part.is_empty() {
889 gravity_parts.push(part);
890 }
891 }
892
893 if let Some(justify) = styles.get("justifyContent") {
895 let part = match justify.as_str() {
896 Some("center") => {
897 if is_horizontal {
898 "center_horizontal"
899 } else {
900 "center_vertical"
901 }
902 }
903 Some("flex-start") | Some("start") => {
904 if is_horizontal {
905 "start"
906 } else {
907 "top"
908 }
909 }
910 Some("flex-end") | Some("end") => {
911 if is_horizontal {
912 "end"
913 } else {
914 "bottom"
915 }
916 }
917 _ => "",
918 };
919 if !part.is_empty() {
920 gravity_parts.push(part);
921 }
922
923 let layout_gravity = match justify.as_str() {
925 Some("center") => "center_horizontal",
926 Some("flex-start") | Some("start") => "start",
927 Some("flex-end") | Some("end") => "end",
928 Some("space-between") | Some("between") => "space_between",
929 Some("space-around") | Some("around") => "space_around",
930 _ => "",
931 };
932 if !layout_gravity.is_empty() {
933 styles.insert(
934 "androidLayoutGravity".to_string(),
935 serde_json::json!(layout_gravity),
936 );
937 }
938 }
939
940 if !gravity_parts.is_empty() {
941 let gravity = if gravity_parts.contains(&"center_vertical")
942 && gravity_parts.contains(&"center_horizontal")
943 {
944 "center".to_string()
945 } else {
946 gravity_parts.join("|")
947 };
948 styles.insert("androidGravity".to_string(), serde_json::json!(gravity));
949 }
950
951 if let Some(serde_json::Value::String(border)) = styles.get("border").cloned() {
953 let parts: Vec<&str> = border.split_whitespace().collect();
954 for part in parts {
955 if part.ends_with("px") {
956 if let Ok(w) = part.trim_end_matches("px").parse::<f32>() {
957 styles.insert(
958 "borderWidth".to_string(),
959 serde_json::json!(dp_to_px(w, density)),
960 );
961 }
962 } else if part.starts_with('#') {
963 styles.insert("borderColor".to_string(), serde_json::json!(part));
964 }
965 }
966 }
967
968 if let Some(serde_json::Value::String(shadow)) = styles.get("boxShadow").cloned() {
970 if !shadow.is_empty() {
971 let elevation = if shadow.contains("20px") {
972 24
973 } else if shadow.contains("15px") {
974 16
975 } else if shadow.contains("10px") {
976 8
977 } else {
978 4
979 };
980 styles.insert(
981 "elevation".to_string(),
982 serde_json::json!(dp_to_px(elevation as f32, density)),
983 );
984 }
985 }
986
987 if let Some(overflow_x) = styles.get("overflowX") {
989 if overflow_x.as_str() == Some("auto") || overflow_x.as_str() == Some("scroll") {
990 styles.insert(
991 "androidScrollHorizontal".to_string(),
992 serde_json::json!(true),
993 );
994 }
995 }
996 if let Some(overflow_y) = styles.get("overflowY") {
997 if overflow_y.as_str() == Some("auto") || overflow_y.as_str() == Some("scroll") {
998 styles.insert("androidScrollVertical".to_string(), serde_json::json!(true));
999 }
1000 }
1001
1002 if let Some(text_align) = styles.get("textAlign") {
1004 let gravity = match text_align.as_str() {
1005 Some("center") => "center_horizontal",
1006 Some("right") | Some("end") => "end",
1007 Some("left") | Some("start") => "start",
1008 _ => "",
1009 };
1010 if !gravity.is_empty() {
1011 styles.insert("androidTextGravity".to_string(), serde_json::json!(gravity));
1012 }
1013 }
1014
1015 if let Some(object_fit) = styles.get("objectFit") {
1017 let scale_type = match object_fit.as_str() {
1018 Some("cover") => "center_crop",
1019 Some("contain") => "fit_center",
1020 Some("fill") => "fit_xy",
1021 Some("none") => "center",
1022 Some("scale-down") => "center_inside",
1023 _ => "",
1024 };
1025 if !scale_type.is_empty() {
1026 styles.insert(
1027 "androidScaleType".to_string(),
1028 serde_json::json!(scale_type),
1029 );
1030 }
1031 }
1032
1033 if let Some(h) = styles.get("height").cloned() {
1035 if h.as_str() == Some("100%") {
1036 styles.insert("height".to_string(), serde_json::json!("match_parent"));
1037 }
1038 }
1039 if let Some(w) = styles.get("width").cloned() {
1040 if w.as_str() == Some("100%") {
1041 styles.insert("width".to_string(), serde_json::json!("match_parent"));
1042 }
1043 }
1044
1045 if styles.contains_key("flex") || styles.contains_key("flexGrow") {
1049 if !styles.contains_key("width") {
1051 styles.insert("width".to_string(), serde_json::json!("wrap_content"));
1052 }
1053 if !styles.contains_key("height") {
1054 styles.insert("height".to_string(), serde_json::json!("wrap_content"));
1055 }
1056 }
1057
1058 if let Some(font_weight) = styles.get("fontWeight") {
1060 let is_bold = match font_weight {
1061 serde_json::Value::String(s) => {
1062 s.contains("bold") || s == "600" || s == "700" || s == "500"
1063 }
1064 serde_json::Value::Number(n) => {
1065 let weight = n.as_i64().unwrap_or(400);
1066 weight >= 500
1067 }
1068 _ => false,
1069 };
1070 if is_bold {
1071 styles.insert(
1072 "androidTypefaceStyle".to_string(),
1073 serde_json::json!("bold"),
1074 );
1075 }
1076 }
1077
1078 if let Some(box_shadow) = styles.get("boxShadow") {
1080 if let Some(shadow_str) = box_shadow.as_str() {
1081 if !shadow_str.is_empty() {
1082 let elevation_dp = if shadow_str.contains("20px") {
1083 24.0
1084 } else if shadow_str.contains("15px") {
1085 16.0
1086 } else if shadow_str.contains("10px") {
1087 8.0
1088 } else if shadow_str.contains("5px") {
1089 4.0
1090 } else {
1091 4.0
1092 };
1093 styles.insert(
1094 "elevation".to_string(),
1095 serde_json::json!(dp_to_px(elevation_dp, density)),
1096 );
1097 }
1098 }
1099 }
1100
1101 styles
1102 }
1103
1104 fn theme_chain(&self) -> Vec<String> {
1108 let mut chain = Vec::new();
1109 let default_name = if self.themes.contains_key(&self.default_theme) {
1111 self.default_theme.clone()
1112 } else if let Some((k, _)) = self.themes.first() {
1113 k.clone()
1114 } else {
1115 return chain;
1116 };
1117 let mut current_name = if self.themes.contains_key(&self.current_theme) {
1118 self.current_theme.clone()
1119 } else {
1120 default_name.clone()
1121 };
1122 let mut seen: IndexSet<String> = IndexSet::new();
1124 let prefers_dark = self
1125 .prefers_color_scheme
1126 .as_deref()
1127 .map(|v| v.eq_ignore_ascii_case("dark"))
1128 .unwrap_or(false);
1129 while !seen.contains(¤t_name) {
1130 seen.insert(current_name.clone());
1131 chain.push(current_name.clone());
1132 let parent = self
1134 .themes
1135 .get(¤t_name)
1136 .and_then(|t| t.parent_name(prefers_dark));
1137 if let Some(p) = parent {
1138 current_name = p;
1139 } else {
1140 break;
1141 }
1142 }
1143 if !chain.iter().any(|n| n == &default_name) {
1144 chain.push(default_name);
1145 }
1146 chain
1147 }
1148
1149 fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
1152 let mut selectors: SelectorStyles = SelectorStyles::new();
1153 let mut vars: IndexMap<String, String> = IndexMap::new();
1154 let chain = self.theme_chain();
1156 for name in chain.into_iter().rev() {
1157 if let Some(entry) = self.themes.get(&name) {
1158 for (sel, props) in entry.selectors.iter() {
1160 if sel.contains(',') {
1162 for s in sel.split(',') {
1163 let s = s.trim();
1164 if s.is_empty() {
1165 continue;
1166 }
1167 let e = selectors.entry(s.to_string()).or_default();
1168 merge_props(e, props);
1169 }
1170 } else {
1171 let e = selectors.entry(sel.clone()).or_default();
1172 merge_props(e, props);
1173 }
1174 }
1175 for (k, v) in entry.variables.iter() {
1177 vars.insert(k.clone(), v.clone());
1178 }
1179 }
1180 }
1181 (selectors, vars)
1182 }
1183
1184 pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
1186 let mut bps: IndexMap<String, String> = IndexMap::new();
1187 let chain = self.theme_chain();
1188 for name in chain.into_iter().rev() {
1189 if let Some(entry) = self.themes.get(&name) {
1190 for (k, v) in entry.breakpoints.iter() {
1191 bps.insert(k.clone(), v.clone());
1192 }
1193 }
1194 }
1195 bps
1196 }
1197}
1198
1199fn split_tag_class_key(key: &str) -> Option<(String, String)> {
1200 let mut it = key.splitn(2, '|');
1201 let t = it.next()?.to_string();
1202 let c = it.next()?.to_string();
1203 if t.is_empty() || c.is_empty() {
1204 return None;
1205 }
1206 Some((t, c))
1207}
1208
1209fn strip_hover_suffix(selector: &str) -> (&str, bool) {
1210 if let Some(stripped) = selector.strip_suffix(":hover") {
1211 (stripped, true)
1212 } else {
1213 (selector, false)
1214 }
1215}
1216
1217fn should_emit_selector(
1218 sel: &str,
1219 used_tags: &IndexSet<String>,
1220 used_classes: &IndexSet<String>,
1221 used_tag_classes: &IndexSet<String>,
1222) -> bool {
1223 let (base, _hover) = strip_hover_suffix(sel);
1225
1226 if is_simple_tag(base) {
1228 return used_tags.contains(base)
1229 || used_tag_classes
1230 .iter()
1231 .any(|k| k.split('|').next() == Some(base));
1232 }
1233
1234 if let Some(class_name) = base.strip_prefix('.') {
1236 return used_classes.contains(class_name)
1238 || used_tag_classes
1239 .iter()
1240 .any(|k| k.ends_with(&format!("|{}", class_name)));
1241 }
1242
1243 if let Some((tag, class_name)) = split_tag_class_selector(base) {
1245 let key = format!("{}|{}", tag, class_name);
1246 return used_tag_classes.contains(&key)
1247 || (used_tags.contains(&tag) && used_classes.contains(&class_name));
1248 }
1249
1250 false
1252}
1253
1254fn is_simple_tag(s: &str) -> bool {
1255 let mut chars = s.chars();
1257 match chars.next() {
1258 Some(c) if c.is_ascii_alphabetic() => {}
1259 _ => return false,
1260 }
1261 chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
1262}
1263
1264fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
1265 let mut parts = s.splitn(2, '.');
1267 let tag = parts.next()?.to_string();
1268 let class_name = parts.next()?.to_string();
1269 if tag.is_empty() || class_name.is_empty() {
1270 return None;
1271 }
1272 Some((tag, class_name))
1273}
1274
1275#[cfg(target_arch = "wasm32")]
1277#[wasm_bindgen]
1278pub fn render_css_for_web(state_json: &str) -> String {
1279 render_css_for_web_impl(state_json)
1280}
1281
1282pub fn render_css_for_web_impl(state_json: &str) -> String {
1284 match serde_json::from_str::<State>(state_json) {
1285 Ok(s) => s.css_for_web(),
1286 Err(_) => "".into(),
1287 }
1288}
1289
1290#[cfg(not(target_arch = "wasm32"))]
1291pub fn render_css_for_web(state_json: &str) -> String {
1292 render_css_for_web_impl(state_json)
1293}
1294
1295#[cfg(target_arch = "wasm32")]
1296#[wasm_bindgen]
1297pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
1298 let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
1299 match serde_json::from_str::<State>(state_json) {
1300 Ok(s) => serde_json::to_string(&s.android_styles_for(selector, &classes))
1301 .unwrap_or_else(|_| "{}".into()),
1302 Err(_) => "{}".into(),
1303 }
1304}
1305
1306#[cfg(target_arch = "wasm32")]
1308#[wasm_bindgen]
1309pub fn get_version() -> String {
1310 env!("CARGO_PKG_VERSION").to_string()
1312}
1313
1314pub fn version() -> &'static str {
1316 env!("CARGO_PKG_VERSION")
1317}
1318
1319pub fn get_default_state_json_impl() -> String {
1321 let st = bundled_state();
1322 match serde_json::to_string(&st.to_json()) {
1323 Ok(s) => s,
1324 Err(_) => "{}".to_string(),
1325 }
1326}
1327
1328#[cfg(target_arch = "wasm32")]
1329#[wasm_bindgen]
1330pub fn get_default_state_json() -> String {
1331 get_default_state_json_impl()
1332}
1333
1334#[cfg(not(target_arch = "wasm32"))]
1335pub fn get_default_state_json() -> String {
1336 get_default_state_json_impl()
1337}
1338
1339pub fn register_theme_json(state_json: &str, theme_json: &str) -> String {
1343 match (
1344 serde_json::from_str::<State>(state_json),
1345 serde_json::from_str::<serde_json::Value>(theme_json),
1346 ) {
1347 (Ok(mut state), Ok(theme_obj)) => {
1348 if let (Some(name), Some(theme_entry)) = (theme_obj.get("name"), theme_obj.get("theme"))
1349 {
1350 if let Ok(entry) = serde_json::from_value::<ThemeEntry>(theme_entry.clone()) {
1351 let theme_name = name.as_str().unwrap_or("").to_string();
1352 if !theme_name.is_empty() {
1353 state.themes.insert(theme_name, entry);
1354 }
1355 }
1356 }
1357 match serde_json::to_string(&state.to_json()) {
1358 Ok(s) => s,
1359 Err(_) => "{}".to_string(),
1360 }
1361 }
1362 _ => "{}".to_string(),
1363 }
1364}
1365
1366#[cfg(target_arch = "wasm32")]
1367#[wasm_bindgen]
1368pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
1369 set_theme_json_native(state_json, theme_name)
1370}
1371
1372#[cfg(not(target_arch = "wasm32"))]
1373pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
1374 set_theme_json_native(state_json, theme_name)
1375}
1376
1377fn set_theme_json_native(state_json: &str, theme_name: &str) -> String {
1378 match serde_json::from_str::<State>(state_json) {
1379 Ok(mut state) => {
1380 if state.themes.contains_key(theme_name) {
1381 state.default_theme = theme_name.to_string();
1382 state.current_theme = theme_name.to_string();
1383 }
1384 match serde_json::to_string(&state.to_json()) {
1385 Ok(s) => s,
1386 Err(_) => "{}".to_string(),
1387 }
1388 }
1389 _ => "{}".to_string(),
1390 }
1391}
1392
1393#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
1396pub fn get_theme_list_json(state_json: &str) -> String {
1397 match serde_json::from_str::<State>(state_json) {
1398 Ok(state) => {
1399 let themes: Vec<serde_json::Value> = state
1400 .themes
1401 .iter()
1402 .map(|(key, entry)| {
1403 json!({
1404 "key": key,
1405 "name": entry.name.as_ref().unwrap_or(key)
1406 })
1407 })
1408 .collect();
1409 serde_json::to_string(&themes).unwrap_or_else(|_| "[]".to_string())
1410 }
1411 _ => "[]".to_string(),
1412 }
1413}
1414
1415fn merge_props(into: &mut CssProps, from: &CssProps) {
1416 for (k, v) in from.iter() {
1417 into.insert(k.clone(), v.clone());
1418 }
1419}
1420
1421fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
1424 let mut buf = String::new();
1425 for (k, v) in props.iter() {
1426 let key = crate::utils::kebab_case(k);
1427 buf.push_str(&key);
1428 buf.push(':');
1429 let val = if v.is_string() {
1430 let s = v.as_str().unwrap();
1431 resolve_vars(s, vars)
1432 } else {
1433 v.to_string()
1434 };
1435 buf.push_str(&val);
1436 if !val.ends_with(';') {
1437 buf.push(';');
1438 }
1439 }
1440 buf
1441}
1442
1443fn parse_var_references(input: &str) -> Vec<(usize, usize, String)> {
1447 let mut results = Vec::new();
1448 let bytes = input.as_bytes();
1449 let mut i = 0;
1450
1451 while i < bytes.len() {
1452 if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"var(" {
1454 let start = i;
1455 i += 4;
1456
1457 while i < bytes.len()
1459 && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r')
1460 {
1461 i += 1;
1462 }
1463
1464 let has_prefix = i + 2 <= bytes.len() && &bytes[i..i + 2] == b"--";
1466 if has_prefix {
1467 i += 2;
1468 }
1469
1470 let name_start = i;
1472 while i < bytes.len() {
1473 let c = bytes[i];
1474 if (c >= b'a' && c <= b'z')
1475 || (c >= b'A' && c <= b'Z')
1476 || (c >= b'0' && c <= b'9')
1477 || c == b'_'
1478 || c == b'.'
1479 || c == b'-'
1480 {
1481 i += 1;
1482 } else {
1483 break;
1484 }
1485 }
1486
1487 let name_end = i;
1488 if name_start < name_end {
1489 while i < bytes.len()
1491 && (bytes[i] == b' '
1492 || bytes[i] == b'\t'
1493 || bytes[i] == b'\n'
1494 || bytes[i] == b'\r')
1495 {
1496 i += 1;
1497 }
1498
1499 if i < bytes.len() && bytes[i] == b')' {
1501 let end = i + 1;
1502 let var_name = std::str::from_utf8(&bytes[name_start..name_end])
1503 .unwrap_or("")
1504 .to_string();
1505 results.push((start, end, var_name));
1506 i = end;
1507 continue;
1508 }
1509 }
1510 }
1511 i += 1;
1512 }
1513
1514 results
1515}
1516
1517static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> =
1519 Lazy::new(|| {
1520 let mut colors = IndexMap::new();
1521
1522 let mut slate = IndexMap::new();
1523 slate.insert("50", "#f8fafc");
1524 slate.insert("100", "#f1f5f9");
1525 slate.insert("200", "#e2e8f0");
1526 slate.insert("300", "#cbd5e1");
1527 slate.insert("400", "#94a3b8");
1528 slate.insert("500", "#64748b");
1529 slate.insert("600", "#475569");
1530 slate.insert("700", "#334155");
1531 slate.insert("800", "#1e293b");
1532 slate.insert("900", "#0f172a");
1533 slate.insert("950", "#020617");
1534 colors.insert("slate", slate);
1535
1536 let mut gray = IndexMap::new();
1537 gray.insert("50", "#f9fafb");
1538 gray.insert("100", "#f3f4f6");
1539 gray.insert("200", "#e5e7eb");
1540 gray.insert("300", "#d1d5db");
1541 gray.insert("400", "#9ca3af");
1542 gray.insert("500", "#6b7280");
1543 gray.insert("600", "#4b5563");
1544 gray.insert("700", "#374151");
1545 gray.insert("800", "#1f2937");
1546 gray.insert("900", "#111827");
1547 gray.insert("950", "#030712");
1548 colors.insert("gray", gray);
1549
1550 let mut zinc = IndexMap::new();
1551 zinc.insert("50", "#fafafa");
1552 zinc.insert("100", "#f4f4f5");
1553 zinc.insert("200", "#e4e4e7");
1554 zinc.insert("300", "#d4d4d8");
1555 zinc.insert("400", "#a1a1aa");
1556 zinc.insert("500", "#71717a");
1557 zinc.insert("600", "#52525b");
1558 zinc.insert("700", "#3f3f46");
1559 zinc.insert("800", "#27272a");
1560 zinc.insert("900", "#18181b");
1561 zinc.insert("950", "#09090b");
1562 colors.insert("zinc", zinc);
1563
1564 let mut neutral = IndexMap::new();
1565 neutral.insert("50", "#fafafa");
1566 neutral.insert("100", "#f5f5f5");
1567 neutral.insert("200", "#e5e5e5");
1568 neutral.insert("300", "#d4d4d4");
1569 neutral.insert("400", "#a3a3a3");
1570 neutral.insert("500", "#737373");
1571 neutral.insert("600", "#525252");
1572 neutral.insert("700", "#404040");
1573 neutral.insert("800", "#262626");
1574 neutral.insert("900", "#171717");
1575 neutral.insert("950", "#0a0a0a");
1576 colors.insert("neutral", neutral);
1577
1578 let mut stone = IndexMap::new();
1579 stone.insert("50", "#fafaf9");
1580 stone.insert("100", "#f5f5f4");
1581 stone.insert("200", "#e7e5e4");
1582 stone.insert("300", "#d6d3d1");
1583 stone.insert("400", "#a8a29e");
1584 stone.insert("500", "#78716c");
1585 stone.insert("600", "#57534e");
1586 stone.insert("700", "#44403c");
1587 stone.insert("800", "#292524");
1588 stone.insert("900", "#1c1917");
1589 stone.insert("950", "#0c0a09");
1590 colors.insert("stone", stone);
1591
1592 let mut red = IndexMap::new();
1593 red.insert("50", "#fef2f2");
1594 red.insert("100", "#fee2e2");
1595 red.insert("200", "#fecaca");
1596 red.insert("300", "#fca5a5");
1597 red.insert("400", "#f87171");
1598 red.insert("500", "#ef4444");
1599 red.insert("600", "#dc2626");
1600 red.insert("700", "#b91c1c");
1601 red.insert("800", "#991b1b");
1602 red.insert("900", "#7f1d1d");
1603 red.insert("950", "#450a0a");
1604 colors.insert("red", red);
1605
1606 let mut orange = IndexMap::new();
1607 orange.insert("50", "#fff7ed");
1608 orange.insert("100", "#ffedd5");
1609 orange.insert("200", "#fed7aa");
1610 orange.insert("300", "#fdba74");
1611 orange.insert("400", "#fb923c");
1612 orange.insert("500", "#f97316");
1613 orange.insert("600", "#ea580c");
1614 orange.insert("700", "#c2410c");
1615 orange.insert("800", "#9a3412");
1616 orange.insert("900", "#7c2d12");
1617 orange.insert("950", "#431407");
1618 colors.insert("orange", orange);
1619
1620 let mut amber = IndexMap::new();
1621 amber.insert("50", "#fffbeb");
1622 amber.insert("100", "#fef3c7");
1623 amber.insert("200", "#fde68a");
1624 amber.insert("300", "#fcd34d");
1625 amber.insert("400", "#fbbf24");
1626 amber.insert("500", "#f59e0b");
1627 amber.insert("600", "#d97706");
1628 amber.insert("700", "#b45309");
1629 amber.insert("800", "#92400e");
1630 amber.insert("900", "#78350f");
1631 amber.insert("950", "#451a03");
1632 colors.insert("amber", amber);
1633
1634 let mut blue = IndexMap::new();
1635 blue.insert("50", "#eff6ff");
1636 blue.insert("100", "#dbeafe");
1637 blue.insert("200", "#bfdbfe");
1638 blue.insert("300", "#93c5fd");
1639 blue.insert("400", "#60a5fa");
1640 blue.insert("500", "#3b82f6");
1641 blue.insert("600", "#2563eb");
1642 blue.insert("700", "#1d4ed8");
1643 blue.insert("800", "#1e40af");
1644 blue.insert("900", "#1e3a8a");
1645 blue.insert("950", "#0b1c52");
1646 colors.insert("blue", blue);
1647
1648 let mut lime = IndexMap::new();
1649 lime.insert("50", "#f7fee7");
1650 lime.insert("100", "#ecfccb");
1651 lime.insert("200", "#d9f99d");
1652 lime.insert("300", "#bef264");
1653 lime.insert("400", "#a3e635");
1654 lime.insert("500", "#84cc16");
1655 lime.insert("600", "#65a30d");
1656 lime.insert("700", "#4d7c0f");
1657 lime.insert("800", "#3f6212");
1658 lime.insert("900", "#365314");
1659 lime.insert("950", "#1a2e05");
1660 colors.insert("lime", lime);
1661
1662 let mut green = IndexMap::new();
1663 green.insert("50", "#f0fdf4");
1664 green.insert("100", "#dcfce7");
1665 green.insert("200", "#bbf7d0");
1666 green.insert("300", "#86efac");
1667 green.insert("400", "#4ade80");
1668 green.insert("500", "#22c55e");
1669 green.insert("600", "#16a34a");
1670 green.insert("700", "#15803d");
1671 green.insert("800", "#166534");
1672 green.insert("900", "#14532d");
1673 green.insert("950", "#052e16");
1674 colors.insert("green", green);
1675
1676 let mut emerald = IndexMap::new();
1677 emerald.insert("50", "#ecfdf5");
1678 emerald.insert("100", "#d1fae5");
1679 emerald.insert("200", "#a7f3d0");
1680 emerald.insert("300", "#6ee7b7");
1681 emerald.insert("400", "#34d399");
1682 emerald.insert("500", "#10b981");
1683 emerald.insert("600", "#059669");
1684 emerald.insert("700", "#047857");
1685 emerald.insert("800", "#065f46");
1686 emerald.insert("900", "#064e3b");
1687 emerald.insert("950", "#022c22");
1688 colors.insert("emerald", emerald);
1689
1690 let mut teal = IndexMap::new();
1691 teal.insert("50", "#f0fdfa");
1692 teal.insert("100", "#ccfbf1");
1693 teal.insert("200", "#99f6e4");
1694 teal.insert("300", "#5eead4");
1695 teal.insert("400", "#2dd4bf");
1696 teal.insert("500", "#14b8a6");
1697 teal.insert("600", "#0d9488");
1698 teal.insert("700", "#0f766e");
1699 teal.insert("800", "#115e59");
1700 teal.insert("900", "#134e4a");
1701 teal.insert("950", "#042f2e");
1702 colors.insert("teal", teal);
1703
1704 let mut cyan = IndexMap::new();
1705 cyan.insert("50", "#ecfeff");
1706 cyan.insert("100", "#cffafe");
1707 cyan.insert("200", "#a5f3fc");
1708 cyan.insert("300", "#67e8f9");
1709 cyan.insert("400", "#22d3ee");
1710 cyan.insert("500", "#06b6d4");
1711 cyan.insert("600", "#0891b2");
1712 cyan.insert("700", "#0e7490");
1713 cyan.insert("800", "#155e75");
1714 cyan.insert("900", "#164e63");
1715 cyan.insert("950", "#083344");
1716 colors.insert("cyan", cyan);
1717
1718 let mut sky = IndexMap::new();
1719 sky.insert("50", "#f0f9ff");
1720 sky.insert("100", "#e0f2fe");
1721 sky.insert("200", "#bae6fd");
1722 sky.insert("300", "#7dd3fc");
1723 sky.insert("400", "#38bdf8");
1724 sky.insert("500", "#0ea5e9");
1725 sky.insert("600", "#0284c7");
1726 sky.insert("700", "#0369a1");
1727 sky.insert("800", "#075985");
1728 sky.insert("900", "#0c4a6e");
1729 sky.insert("950", "#082f49");
1730 colors.insert("sky", sky);
1731
1732 let mut blue = IndexMap::new();
1733 blue.insert("50", "#eff6ff");
1734 blue.insert("100", "#dbeafe");
1735 blue.insert("200", "#bfdbfe");
1736 blue.insert("300", "#93c5fd");
1737 blue.insert("400", "#60a5fa");
1738 blue.insert("500", "#3b82f6");
1739 blue.insert("600", "#2563eb");
1740 blue.insert("700", "#1d4ed8");
1741 blue.insert("800", "#1e40af");
1742 blue.insert("900", "#1e3a8a");
1743 blue.insert("950", "#172554");
1744 colors.insert("blue", blue);
1745
1746 let mut indigo = IndexMap::new();
1747 indigo.insert("50", "#eef2ff");
1748 indigo.insert("100", "#e0e7ff");
1749 indigo.insert("200", "#c7d2fe");
1750 indigo.insert("300", "#a5b4fc");
1751 indigo.insert("400", "#818cf8");
1752 indigo.insert("500", "#6366f1");
1753 indigo.insert("600", "#4f46e5");
1754 indigo.insert("700", "#4338ca");
1755 indigo.insert("800", "#3730a3");
1756 indigo.insert("900", "#312e81");
1757 indigo.insert("950", "#1e1b4b");
1758 colors.insert("indigo", indigo);
1759
1760 let mut violet = IndexMap::new();
1761 violet.insert("50", "#f5f3ff");
1762 violet.insert("100", "#ede9fe");
1763 violet.insert("200", "#ddd6fe");
1764 violet.insert("300", "#c4b5fd");
1765 violet.insert("400", "#a78bfa");
1766 violet.insert("500", "#8b5cf6");
1767 violet.insert("600", "#7c3aed");
1768 violet.insert("700", "#6d28d9");
1769 violet.insert("800", "#5b21b6");
1770 violet.insert("900", "#4c1d95");
1771 violet.insert("950", "#2e1065");
1772 colors.insert("violet", violet);
1773
1774 let mut purple = IndexMap::new();
1775 purple.insert("50", "#faf5ff");
1776 purple.insert("100", "#f3e8ff");
1777 purple.insert("200", "#e9d5ff");
1778 purple.insert("300", "#d8b4fe");
1779 purple.insert("400", "#c084fc");
1780 purple.insert("500", "#a855f7");
1781 purple.insert("600", "#9333ea");
1782 purple.insert("700", "#7e22ce");
1783 purple.insert("800", "#6b21a8");
1784 purple.insert("900", "#581c87");
1785 purple.insert("950", "#3b0764");
1786 colors.insert("purple", purple);
1787
1788 let mut fuchsia = IndexMap::new();
1789 fuchsia.insert("50", "#fdf4ff");
1790 fuchsia.insert("100", "#fae8ff");
1791 fuchsia.insert("200", "#f5d0fe");
1792 fuchsia.insert("300", "#f0abfc");
1793 fuchsia.insert("400", "#e879f9");
1794 fuchsia.insert("500", "#d946ef");
1795 fuchsia.insert("600", "#c026d3");
1796 fuchsia.insert("700", "#a21caf");
1797 fuchsia.insert("800", "#86198f");
1798 fuchsia.insert("900", "#701a75");
1799 fuchsia.insert("950", "#4a044e");
1800 colors.insert("fuchsia", fuchsia);
1801
1802 let mut pink = IndexMap::new();
1803 pink.insert("50", "#fdf2f8");
1804 pink.insert("100", "#fce7f3");
1805 pink.insert("200", "#fbcfe8");
1806 pink.insert("300", "#f9a8d4");
1807 pink.insert("400", "#f472b6");
1808 pink.insert("500", "#ec4899");
1809 pink.insert("600", "#db2777");
1810 pink.insert("700", "#be185d");
1811 pink.insert("800", "#9d174d");
1812 pink.insert("900", "#831843");
1813 pink.insert("950", "#500724");
1814 colors.insert("pink", pink);
1815
1816 let mut rose = IndexMap::new();
1817 rose.insert("50", "#fff1f2");
1818 rose.insert("100", "#ffe4e6");
1819 rose.insert("200", "#fecdd3");
1820 rose.insert("300", "#fda4af");
1821 rose.insert("400", "#fb7185");
1822 rose.insert("500", "#f43f5e");
1823 rose.insert("600", "#e11d48");
1824 rose.insert("700", "#be123c");
1825 rose.insert("800", "#9f1239");
1826 rose.insert("900", "#881337");
1827 rose.insert("950", "#4c0519");
1828 colors.insert("rose", rose);
1829
1830 colors
1831 });
1832
1833fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
1834 let var_refs = parse_var_references(input);
1835
1836 if var_refs.is_empty() {
1837 if input.starts_with('$') {
1839 if let Some(val) = vars.get(&input[1..]) {
1840 return val.clone();
1841 }
1842 }
1843 return input.to_string();
1844 }
1845
1846 let mut out = input.to_string();
1848 for (start, end, var_name) in var_refs.iter().rev() {
1849 if let Some(val) = vars.get(var_name) {
1850 out.replace_range(*start..*end, val);
1851 }
1852 }
1853
1854 if out.starts_with('$') {
1856 if let Some(val) = vars.get(&out[1..]) {
1857 return val.clone();
1858 }
1859 }
1860
1861 out
1862}
1863
1864fn camel_case(name: &str) -> String {
1865 let mut out = String::new();
1866 let mut upper = false;
1867 for ch in name.chars() {
1868 if ch == '-' {
1869 upper = true;
1870 continue;
1871 }
1872 if upper {
1873 out.extend(ch.to_uppercase());
1874 upper = false;
1875 } else {
1876 out.push(ch);
1877 }
1878 }
1879 out
1880}
1881
1882fn css_value_to_android(
1883 value: &serde_json::Value,
1884 vars: &IndexMap<String, String>,
1885 current_color: Option<&str>,
1886) -> serde_json::Value {
1887 if let Some(s) = value.as_str() {
1888 if s.contains("color-mix") {
1889 log::debug!(
1890 "[css_value_to_android] color-mix detected: {} current_color={:?}",
1891 s,
1892 current_color
1893 );
1894 }
1895 }
1896 match value {
1897 serde_json::Value::String(s) => {
1898 let s2 = resolve_vars(s, vars);
1899 let s3 = color::resolve_color(&s2, current_color, vars);
1900 if let Some(n) = s3.strip_suffix("px") {
1901 if let Ok(parsed) = n.trim().parse::<f64>() {
1902 return json!(parsed);
1903 }
1904 }
1905 json!(s3)
1906 }
1907 _ => value.clone(),
1908 }
1909}
1910
1911fn css_value_to_web(
1913 value: &serde_json::Value,
1914 vars: &IndexMap<String, String>,
1915 current_color: Option<&str>,
1916) -> serde_json::Value {
1917 match value {
1918 serde_json::Value::String(s) => {
1919 let s2 = resolve_vars(s, vars);
1920 let s3 = color::resolve_color(&s2, current_color, vars);
1921 let s4 = match s3.as_str() {
1922 "match_parent" => "100%".to_string(),
1923 "wrap_content" | "fit-content" => "auto".to_string(),
1924 _ => s3,
1925 };
1926 json!(s4)
1927 }
1928 _ => value.clone(),
1929 }
1930}
1931
1932fn merge_web_props(
1934 into: &mut IndexMap<String, serde_json::Value>,
1935 css_props: &CssProps,
1936 vars: &IndexMap<String, String>,
1937) {
1938 let current_color = css_props
1939 .get("color")
1940 .and_then(|v| v.as_str())
1941 .map(|s| resolve_vars(s, vars))
1942 .or_else(|| into.get("color").and_then(|v| v.as_str()).map(|s| s.to_string()));
1943
1944 for (k, v) in css_props.iter() {
1945 let val = css_value_to_web(v, vars, current_color.as_deref());
1946
1947 match k.as_str() {
1948 "padding" => {
1949 into.insert("paddingTop".to_string(), val.clone());
1950 into.insert("paddingBottom".to_string(), val.clone());
1951 into.insert("paddingLeft".to_string(), val.clone());
1952 into.insert("paddingRight".to_string(), val.clone());
1953 into.insert("padding".to_string(), val);
1954 }
1955 "padding-horizontal" | "paddingHorizontal" => {
1956 into.insert("paddingLeft".to_string(), val.clone());
1957 into.insert("paddingRight".to_string(), val.clone());
1958 }
1959 "padding-vertical" | "paddingVertical" => {
1960 into.insert("paddingTop".to_string(), val.clone());
1961 into.insert("paddingBottom".to_string(), val.clone());
1962 }
1963 "margin" => {
1964 into.insert("marginTop".to_string(), val.clone());
1965 into.insert("marginBottom".to_string(), val.clone());
1966 into.insert("marginLeft".to_string(), val.clone());
1967 into.insert("marginRight".to_string(), val.clone());
1968 into.insert("margin".to_string(), val);
1969 }
1970 "margin-horizontal" | "marginHorizontal" => {
1971 into.insert("marginLeft".to_string(), val.clone());
1972 into.insert("marginRight".to_string(), val.clone());
1973 }
1974 "margin-vertical" | "marginVertical" => {
1975 into.insert("marginTop".to_string(), val.clone());
1976 into.insert("marginBottom".to_string(), val.clone());
1977 }
1978 "border-radius" | "borderRadius" => {
1979 into.insert("borderRadius".to_string(), val);
1980 }
1981 "background-color" => {
1982 into.insert("backgroundColor".to_string(), val);
1983 }
1984 "text-align" => {
1985 into.insert("textAlign".to_string(), val);
1986 }
1987 "flex-direction" | "flexDirection" => {
1988 into.insert("flexDirection".to_string(), val);
1989 }
1990 "android-gravity" | "androidOrientation" | "androidGravity" | "androidLayoutGravity"
1991 | "androidFlexWrap" | "androidAlpha" | "androidScrollHorizontal"
1992 | "androidScrollVertical" | "androidTextGravity" => {}
1993 "--space-x" => {
1994 into.insert("spaceX".to_string(), val);
1995 }
1996 "--space-y" => {
1997 into.insert("spaceY".to_string(), val);
1998 }
1999 _ => {
2000 into.insert(camel_case(k), val);
2001 }
2002 }
2003 }
2004}
2005
2006fn merge_android_props(
2007 into: &mut IndexMap<String, serde_json::Value>,
2008 css_props: &CssProps,
2009 vars: &IndexMap<String, String>,
2010) {
2011 log::debug!(
2012 "[merge_android_props] START props_count={}",
2013 css_props.len()
2014 );
2015 let mut current_color = css_props
2017 .get("color")
2018 .and_then(|v| v.as_str())
2019 .map(|s| resolve_vars(s, vars));
2020
2021 if current_color.is_none() {
2022 current_color = into
2023 .get("color")
2024 .and_then(|v| v.as_str())
2025 .map(|s| s.to_string());
2026 }
2027
2028 if let Some(ref c) = current_color {
2029 log::debug!("[merge_android_props] current_color resolved to: {}", c);
2030 }
2031
2032 for (k, v) in css_props.iter() {
2033 let val = css_value_to_android(v, vars, current_color.as_deref());
2034
2035 if k == "placeholder-color" || k == "placeholderColor" {
2036 log::debug!(
2037 "[merge_android_props] placeholder-color: input={:?} output={:?}",
2038 v,
2039 val
2040 );
2041 }
2042
2043 match k.as_str() {
2044 "padding" => {
2045 into.insert("paddingTop".to_string(), val.clone());
2046 into.insert("paddingBottom".to_string(), val.clone());
2047 into.insert("paddingLeft".to_string(), val.clone());
2048 into.insert("paddingRight".to_string(), val.clone());
2049 into.insert("paddingHorizontal".to_string(), val.clone());
2050 into.insert("paddingVertical".to_string(), val.clone());
2051 into.insert("padding".to_string(), val);
2052 }
2053 "padding-horizontal" | "paddingHorizontal" => {
2054 into.insert("paddingLeft".to_string(), val.clone());
2055 into.insert("paddingRight".to_string(), val.clone());
2056 into.insert("paddingHorizontal".to_string(), val);
2057 }
2058 "padding-vertical" | "paddingVertical" => {
2059 into.insert("paddingTop".to_string(), val.clone());
2060 into.insert("paddingBottom".to_string(), val.clone());
2061 into.insert("paddingVertical".to_string(), val);
2062 }
2063 "margin" => {
2064 into.insert("marginTop".to_string(), val.clone());
2065 into.insert("marginBottom".to_string(), val.clone());
2066 into.insert("marginLeft".to_string(), val.clone());
2067 into.insert("marginRight".to_string(), val.clone());
2068 into.insert("marginHorizontal".to_string(), val.clone());
2069 into.insert("marginVertical".to_string(), val.clone());
2070 into.insert("margin".to_string(), val);
2071 }
2072 "margin-horizontal" | "marginHorizontal" => {
2073 into.insert("marginLeft".to_string(), val.clone());
2074 into.insert("marginRight".to_string(), val.clone());
2075 into.insert("marginHorizontal".to_string(), val);
2076 }
2077 "margin-vertical" | "marginVertical" => {
2078 into.insert("marginTop".to_string(), val.clone());
2079 into.insert("marginBottom".to_string(), val.clone());
2080 into.insert("marginVertical".to_string(), val);
2081 }
2082 "border-radius" | "borderRadius" => {
2083 into.insert("borderTopLeftRadius".to_string(), val.clone());
2084 into.insert("borderTopRightRadius".to_string(), val.clone());
2085 into.insert("borderBottomLeftRadius".to_string(), val.clone());
2086 into.insert("borderBottomRightRadius".to_string(), val.clone());
2087 into.insert("borderRadius".to_string(), val);
2088 }
2089 "background-color" => {
2090 into.insert("backgroundColor".to_string(), val);
2091 }
2092 "text-align" => {
2093 into.insert("textAlign".to_string(), val);
2094 }
2095 "flex-direction" | "flexDirection" => {
2096 let orientation =
2097 if val.as_str() == Some("column") || val.as_str() == Some("column-reverse") {
2098 "vertical"
2099 } else {
2100 "horizontal"
2101 };
2102 into.insert(
2103 "androidOrientation".to_string(),
2104 serde_json::json!(orientation),
2105 );
2106 into.insert("flexDirection".to_string(), val);
2107 }
2108 "--space-x" => {
2109 into.insert("spaceX".to_string(), val);
2110 }
2111 "--space-y" => {
2112 into.insert("spaceY".to_string(), val);
2113 }
2114 _ => {
2115 into.insert(camel_case(k), val);
2116 }
2117 }
2118 }
2119}
2120
2121fn dynamic_css_properties_for_class(
2122 class: &str,
2123 vars: &IndexMap<String, String>,
2124) -> Option<CssProps> {
2125 match class {
2127 "block" => {
2128 let mut p = CssProps::new();
2129 p.insert("display".into(), json!("block"));
2130 return Some(p);
2131 }
2132 "inline-block" => {
2133 let mut p = CssProps::new();
2134 p.insert("display".into(), json!("inline-block"));
2135 return Some(p);
2136 }
2137 "inline" => {
2138 let mut p = CssProps::new();
2139 p.insert("display".into(), json!("inline"));
2140 return Some(p);
2141 }
2142 "inline-flex" => {
2143 let mut p = CssProps::new();
2144 p.insert("display".into(), json!("inline-flex"));
2145 return Some(p);
2146 }
2147 "grid" => {
2148 let mut p = CssProps::new();
2149 p.insert("display".into(), json!("grid"));
2150 return Some(p);
2151 }
2152 "hidden" => {
2153 let mut p = CssProps::new();
2154 p.insert("display".into(), json!("none"));
2155 return Some(p);
2156 }
2157 _ => {}
2158 }
2159 match class {
2161 "flex" => {
2162 let mut p = CssProps::new();
2163 p.insert("display".into(), json!("flex"));
2164 return Some(p);
2165 }
2166 "flex-row" => {
2167 let mut p = CssProps::new();
2168 p.insert("display".into(), json!("flex"));
2169 p.insert("flexDirection".into(), json!("row"));
2170 return Some(p);
2171 }
2172 "flex-col" => {
2173 let mut p = CssProps::new();
2174 p.insert("display".into(), json!("flex"));
2175 p.insert("flexDirection".into(), json!("column"));
2176 return Some(p);
2177 }
2178 "flex-wrap" => {
2179 let mut p = CssProps::new();
2180 p.insert("display".into(), json!("flex"));
2181 p.insert("flex-wrap".into(), json!("wrap"));
2182 return Some(p);
2183 }
2184 "flex-nowrap" => {
2185 let mut p = CssProps::new();
2186 p.insert("display".into(), json!("flex"));
2187 p.insert("flex-wrap".into(), json!("nowrap"));
2188 return Some(p);
2189 }
2190 "flex-wrap-reverse" => {
2191 let mut p = CssProps::new();
2192 p.insert("display".into(), json!("flex"));
2193 p.insert("flex-wrap".into(), json!("wrap-reverse"));
2194 return Some(p);
2195 }
2196 "flex-1" => {
2197 let mut p = CssProps::new();
2198 p.insert("flex".into(), json!(1));
2199 return Some(p);
2200 }
2201 "w-full" => {
2202 let mut p = CssProps::new();
2203 p.insert("width".into(), json!("match_parent"));
2204 return Some(p);
2205 }
2206 "h-full" => {
2207 let mut p = CssProps::new();
2208 p.insert("height".into(), json!("match_parent"));
2209 return Some(p);
2210 }
2211 _ => {}
2212 }
2213 if let Some(value) = class.strip_prefix("z-") {
2214 if let Ok(z) = value.parse::<i32>() {
2215 let mut p = CssProps::new();
2216 p.insert("elevation".into(), json!(z));
2217 return Some(p);
2218 }
2219 }
2220 if let Some(rest) = class.strip_prefix("items-") {
2221 let mut p = CssProps::new();
2222 let v = match rest {
2223 "start" => "flex-start",
2224 "end" => "flex-end",
2225 "center" => "center",
2226 "stretch" => "stretch",
2227 other => other,
2228 };
2229 p.insert("align-items".into(), json!(v));
2230 return Some(p);
2231 }
2232 if let Some(rest) = class.strip_prefix("justify-") {
2233 let mut p = CssProps::new();
2234 let v = match rest {
2235 "start" => "flex-start",
2236 "end" => "flex-end",
2237 "center" => "center",
2238 "between" => "space-between",
2239 "around" => "space-around",
2240 "evenly" => "space-evenly",
2241 other => other,
2242 };
2243 p.insert("justify-content".into(), json!(v));
2244 return Some(p);
2245 }
2246 if let Some(value) = class.strip_prefix("p-") {
2247 return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
2248 }
2249 if let Some(value) = class.strip_prefix("px-") {
2250 return parse_tailwind_spacing(value, &|px| {
2251 padding_props(&["padding-left", "padding-right"], px)
2252 });
2253 }
2254 if let Some(value) = class.strip_prefix("py-") {
2255 return parse_tailwind_spacing(value, &|px| {
2256 padding_props(&["padding-top", "padding-bottom"], px)
2257 });
2258 }
2259 for &(prefix, prop) in &[
2260 ("pt-", "padding-top"),
2261 ("pr-", "padding-right"),
2262 ("pb-", "padding-bottom"),
2263 ("pl-", "padding-left"),
2264 ] {
2265 if let Some(value) = class.strip_prefix(prefix) {
2266 return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
2267 }
2268 }
2269 if let Some(value) = class.strip_prefix("m-") {
2271 if value == "auto" {
2272 let mut p = CssProps::new();
2273 p.insert("margin".into(), json!("auto"));
2274 return Some(p);
2275 }
2276 return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
2277 }
2278 if let Some(value) = class.strip_prefix("mx-") {
2279 if value == "auto" {
2280 let mut p = CssProps::new();
2281 p.insert("margin-left".into(), json!("auto"));
2282 p.insert("margin-right".into(), json!("auto"));
2283 return Some(p);
2284 }
2285 return parse_tailwind_spacing(value, &|px| {
2286 margin_props(&["margin-left", "margin-right"], px)
2287 });
2288 }
2289 if let Some(value) = class.strip_prefix("my-") {
2290 if value == "auto" {
2291 let mut p = CssProps::new();
2292 p.insert("margin-top".into(), json!("auto"));
2293 p.insert("margin-bottom".into(), json!("auto"));
2294 return Some(p);
2295 }
2296 return parse_tailwind_spacing(value, &|px| {
2297 margin_props(&["margin-top", "margin-bottom"], px)
2298 });
2299 }
2300 for &(prefix, prop) in &[
2301 ("mt-", "margin-top"),
2302 ("mr-", "margin-right"),
2303 ("mb-", "margin-bottom"),
2304 ("ml-", "margin-left"),
2305 ] {
2306 if let Some(value) = class.strip_prefix(prefix) {
2307 if value == "auto" {
2308 let mut p = CssProps::new();
2309 p.insert(prop.into(), json!("auto"));
2310 return Some(p);
2311 }
2312 return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
2313 }
2314 }
2315 if let Some(value) = class.strip_prefix("gap-") {
2317 if !value.starts_with("x-") && !value.starts_with("y-") {
2318 return parse_tailwind_spacing(value, &|px| {
2319 let mut props = CssProps::new();
2320 props.insert("gap".into(), json!(format!("{}px", px)));
2321 props
2322 });
2323 }
2324 }
2325 if let Some(value) = class.strip_prefix("gap-x-") {
2326 return parse_tailwind_spacing(value, &|px| {
2327 let mut props = CssProps::new();
2328 props.insert("column-gap".into(), json!(format!("{}px", px)));
2329 props
2330 });
2331 }
2332 if let Some(value) = class.strip_prefix("gap-y-") {
2333 return parse_tailwind_spacing(value, &|px| {
2334 let mut props = CssProps::new();
2335 props.insert("row-gap".into(), json!(format!("{}px", px)));
2336 props
2337 });
2338 }
2339 if let Some(value) = class.strip_prefix("space-x-") {
2341 return parse_tailwind_spacing(value, &|px| {
2342 let mut props = CssProps::new();
2343 props.insert("--space-x".into(), json!(format!("{}px", px)));
2346 props
2347 });
2348 }
2349 if let Some(value) = class.strip_prefix("space-y-") {
2350 return parse_tailwind_spacing(value, &|px| {
2351 let mut props = CssProps::new();
2352 props.insert("--space-y".into(), json!(format!("{}px", px)));
2353 props
2354 });
2355 }
2356 match class {
2358 "font-thin" => {
2359 let mut p = CssProps::new();
2360 p.insert("font-weight".into(), json!("100"));
2361 return Some(p);
2362 }
2363 "font-extralight" => {
2364 let mut p = CssProps::new();
2365 p.insert("font-weight".into(), json!("200"));
2366 return Some(p);
2367 }
2368 "font-light" => {
2369 let mut p = CssProps::new();
2370 p.insert("font-weight".into(), json!("300"));
2371 return Some(p);
2372 }
2373 "font-normal" => {
2374 let mut p = CssProps::new();
2375 p.insert("font-weight".into(), json!("400"));
2376 return Some(p);
2377 }
2378 "font-medium" => {
2379 let mut p = CssProps::new();
2380 p.insert("font-weight".into(), json!("500"));
2381 return Some(p);
2382 }
2383 "font-semibold" => {
2384 let mut p = CssProps::new();
2385 p.insert("font-weight".into(), json!("600"));
2386 return Some(p);
2387 }
2388 "font-bold" => {
2389 let mut p = CssProps::new();
2390 p.insert("font-weight".into(), json!("700"));
2391 return Some(p);
2392 }
2393 "font-extrabold" => {
2394 let mut p = CssProps::new();
2395 p.insert("font-weight".into(), json!("800"));
2396 return Some(p);
2397 }
2398 "font-black" => {
2399 let mut p = CssProps::new();
2400 p.insert("font-weight".into(), json!("900"));
2401 return Some(p);
2402 }
2403 _ => {}
2404 }
2405 match class {
2407 "font-sans" => {
2408 let mut p = CssProps::new();
2409 p.insert(
2410 "font-family".into(),
2411 json!("system-ui, -apple-system, sans-serif"),
2412 );
2413 return Some(p);
2414 }
2415 "font-serif" => {
2416 let mut p = CssProps::new();
2417 p.insert("font-family".into(), json!("Georgia, serif"));
2418 return Some(p);
2419 }
2420 "font-mono" => {
2421 let mut p = CssProps::new();
2422 p.insert("font-family".into(), json!("ui-monospace, monospace"));
2423 return Some(p);
2424 }
2425 _ => {}
2426 }
2427 match class {
2429 "text-xs" => {
2430 let mut p = CssProps::new();
2431 p.insert("font-size".into(), json!("12px"));
2432 p.insert("line-height".into(), json!("16px"));
2433 return Some(p);
2434 }
2435 "text-sm" => {
2436 let mut p = CssProps::new();
2437 p.insert("font-size".into(), json!("14px"));
2438 p.insert("line-height".into(), json!("20px"));
2439 return Some(p);
2440 }
2441 "text-base" => {
2442 let mut p = CssProps::new();
2443 p.insert("font-size".into(), json!("16px"));
2444 p.insert("line-height".into(), json!("24px"));
2445 return Some(p);
2446 }
2447 "text-lg" => {
2448 let mut p = CssProps::new();
2449 p.insert("font-size".into(), json!("18px"));
2450 p.insert("line-height".into(), json!("28px"));
2451 return Some(p);
2452 }
2453 "text-xl" => {
2454 let mut p = CssProps::new();
2455 p.insert("font-size".into(), json!("20px"));
2456 p.insert("line-height".into(), json!("28px"));
2457 return Some(p);
2458 }
2459 "text-2xl" => {
2460 let mut p = CssProps::new();
2461 p.insert("font-size".into(), json!("24px"));
2462 p.insert("line-height".into(), json!("32px"));
2463 return Some(p);
2464 }
2465 "text-3xl" => {
2466 let mut p = CssProps::new();
2467 p.insert("font-size".into(), json!("30px"));
2468 p.insert("line-height".into(), json!("36px"));
2469 return Some(p);
2470 }
2471 "text-4xl" => {
2472 let mut p = CssProps::new();
2473 p.insert("font-size".into(), json!("36px"));
2474 p.insert("line-height".into(), json!("40px"));
2475 return Some(p);
2476 }
2477 "text-5xl" => {
2478 let mut p = CssProps::new();
2479 p.insert("font-size".into(), json!("48px"));
2480 p.insert("line-height".into(), json!("1"));
2481 return Some(p);
2482 }
2483 "text-6xl" => {
2484 let mut p = CssProps::new();
2485 p.insert("font-size".into(), json!("60px"));
2486 p.insert("line-height".into(), json!("1"));
2487 return Some(p);
2488 }
2489 _ => {}
2490 }
2491 match class {
2493 "text-left" => {
2494 let mut p = CssProps::new();
2495 p.insert("text-align".into(), json!("left"));
2496 return Some(p);
2497 }
2498 "text-center" => {
2499 let mut p = CssProps::new();
2500 p.insert("text-align".into(), json!("center"));
2501 return Some(p);
2502 }
2503 "text-right" => {
2504 let mut p = CssProps::new();
2505 p.insert("text-align".into(), json!("right"));
2506 return Some(p);
2507 }
2508 "text-justify" => {
2509 let mut p = CssProps::new();
2510 p.insert("text-align".into(), json!("justify"));
2511 return Some(p);
2512 }
2513 _ => {}
2514 }
2515 match class {
2517 "overflow-auto" => {
2518 let mut p = CssProps::new();
2519 p.insert("overflow".into(), json!("auto"));
2520 return Some(p);
2521 }
2522 "overflow-hidden" => {
2523 let mut p = CssProps::new();
2524 p.insert("overflow".into(), json!("hidden"));
2525 return Some(p);
2526 }
2527 "overflow-visible" => {
2528 let mut p = CssProps::new();
2529 p.insert("overflow".into(), json!("visible"));
2530 return Some(p);
2531 }
2532 "overflow-scroll" => {
2533 let mut p = CssProps::new();
2534 p.insert("overflow".into(), json!("scroll"));
2535 return Some(p);
2536 }
2537 "overflow-x-auto" => {
2538 let mut p = CssProps::new();
2539 p.insert("overflow-x".into(), json!("auto"));
2540 return Some(p);
2541 }
2542 "overflow-x-hidden" => {
2543 let mut p = CssProps::new();
2544 p.insert("overflow-x".into(), json!("hidden"));
2545 return Some(p);
2546 }
2547 "overflow-x-scroll" => {
2548 let mut p = CssProps::new();
2549 p.insert("overflow-x".into(), json!("scroll"));
2550 return Some(p);
2551 }
2552 "overflow-y-auto" => {
2553 let mut p = CssProps::new();
2554 p.insert("overflow-y".into(), json!("auto"));
2555 return Some(p);
2556 }
2557 "overflow-y-hidden" => {
2558 let mut p = CssProps::new();
2559 p.insert("overflow-y".into(), json!("hidden"));
2560 return Some(p);
2561 }
2562 "overflow-y-scroll" => {
2563 let mut p = CssProps::new();
2564 p.insert("overflow-y".into(), json!("scroll"));
2565 return Some(p);
2566 }
2567 _ => {}
2568 }
2569 if let Some(value) = class.strip_prefix("opacity-") {
2571 if let Ok(opacity) = value.parse::<f32>() {
2572 let mut p = CssProps::new();
2573 p.insert("opacity".into(), json!(opacity / 100.0));
2574 return Some(p);
2575 }
2576 }
2577 match class {
2579 "shadow-sm" => {
2580 let mut p = CssProps::new();
2581 p.insert(
2582 "box-shadow".into(),
2583 json!("0 1px 2px 0 rgba(0, 0, 0, 0.05)"),
2584 );
2585 return Some(p);
2586 }
2587 "shadow" => {
2588 let mut p = CssProps::new();
2589 p.insert(
2590 "box-shadow".into(),
2591 json!("0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"),
2592 );
2593 return Some(p);
2594 }
2595 "shadow-md" => {
2596 let mut p = CssProps::new();
2597 p.insert(
2598 "box-shadow".into(),
2599 json!("0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)"),
2600 );
2601 return Some(p);
2602 }
2603 "shadow-lg" => {
2604 let mut p = CssProps::new();
2605 p.insert(
2606 "box-shadow".into(),
2607 json!("0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)"),
2608 );
2609 return Some(p);
2610 }
2611 "shadow-xl" => {
2612 let mut p = CssProps::new();
2613 p.insert(
2614 "box-shadow".into(),
2615 json!("0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"),
2616 );
2617 return Some(p);
2618 }
2619 "shadow-2xl" => {
2620 let mut p = CssProps::new();
2621 p.insert(
2622 "box-shadow".into(),
2623 json!("0 25px 50px -12px rgba(0, 0, 0, 0.25)"),
2624 );
2625 return Some(p);
2626 }
2627 "shadow-none" => {
2628 let mut p = CssProps::new();
2629 p.insert("box-shadow".into(), json!("none"));
2630 return Some(p);
2631 }
2632 _ => {}
2633 }
2634 if let Some(arb_value) = parse_arbitrary_value(class) {
2636 return Some(arb_value);
2637 }
2638 if let Some(rest) = class.strip_prefix("text-") {
2640 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
2641 let mut props = CssProps::new();
2642 props.insert("color".into(), json!(hex));
2643 return Some(props);
2644 }
2645 }
2646 if let Some(rest) = class.strip_prefix("bg-") {
2648 match rest {
2649 "white" => {
2650 let mut p = CssProps::new();
2651 p.insert("background-color".into(), json!("#ffffff"));
2652 return Some(p);
2653 }
2654 "black" => {
2655 let mut p = CssProps::new();
2656 p.insert("background-color".into(), json!("#000000"));
2657 return Some(p);
2658 }
2659 "transparent" => {
2660 let mut p = CssProps::new();
2661 p.insert("background-color".into(), json!("#00000000"));
2662 return Some(p);
2663 }
2664 _ => {}
2665 }
2666 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
2667 let mut props = CssProps::new();
2668 props.insert("background-color".into(), json!(hex));
2669 return Some(props);
2670 }
2671 }
2672 if let Some(rest) = class.strip_prefix("divide-") {
2674 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
2675 let mut props = CssProps::new();
2676 props.insert("border-color".into(), json!(hex));
2677 return Some(props);
2678 }
2679 }
2680 if class == "border" {
2681 return Some(border_props(None, 1, vars));
2682 }
2683 if let Some(rest) = class.strip_prefix("border-") {
2684 let parts: Vec<&str> = rest.split('-').collect();
2692
2693 let valid_sides = ["t", "b", "l", "r", "x", "y"];
2695 let (side, color_or_width_parts) = if parts.len() > 1 && valid_sides.contains(&parts[0]) {
2696 (Some(parts[0]), &parts[1..])
2697 } else {
2698 (None, &parts[..])
2699 };
2700
2701 if color_or_width_parts.len() == 2 {
2703 let color_shade = color_or_width_parts.join("-");
2705 if let Some(hex) = get_tailwind_color_with_vars(&color_shade, vars) {
2706 let mut props = CssProps::new();
2707 let prop_name = if let Some(s) = side {
2708 format!("border-{}-color", s)
2709 } else {
2710 "border-color".to_string()
2711 };
2712 props.insert(prop_name, json!(hex));
2713 return Some(props);
2714 }
2715 }
2716
2717 if color_or_width_parts.len() == 1 {
2719 let potential_color = format!("{}-500", color_or_width_parts[0]);
2720 if let Some(hex) = get_tailwind_color_with_vars(&potential_color, vars) {
2721 let mut props = CssProps::new();
2722 let prop_name = if let Some(s) = side {
2723 format!("border-{}-color", s)
2724 } else {
2725 "border-color".to_string()
2726 };
2727 props.insert(prop_name, json!(hex));
2728 return Some(props);
2729 }
2730 }
2731
2732 if color_or_width_parts.len() == 1 {
2734 if let Ok(width) = color_or_width_parts[0].parse::<i32>() {
2735 return Some(border_props(side, width, vars));
2736 }
2737 }
2738 }
2739 if class == "rounded" {
2741 return Some(rounded_props(None, Some("md")));
2742 }
2743 if let Some(sz) = class.strip_prefix("rounded-") {
2744 return Some(rounded_props(None, Some(sz)));
2745 }
2746 for &(pref, side) in &[
2747 ("rounded-t", "t"),
2748 ("rounded-b", "b"),
2749 ("rounded-l", "l"),
2750 ("rounded-r", "r"),
2751 ] {
2752 if class == pref {
2753 return Some(rounded_props(Some(side), Some("md")));
2754 }
2755 if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
2756 return Some(rounded_props(Some(side), Some(sz)));
2757 }
2758 }
2759 if let Some(cur) = class.strip_prefix("cursor-") {
2761 let mut props = CssProps::new();
2762 props.insert(
2763 "cursor".into(),
2764 json!(match cur {
2765 "pointer" => "pointer",
2766 "default" => "default",
2767 "text" => "text",
2768 "move" => "move",
2769 "wait" => "wait",
2770 "not-allowed" => "not-allowed",
2771 other => other,
2772 }),
2773 );
2774 return Some(props);
2775 }
2776 if class == "transition" || class == "transition-all" {
2778 let mut props = CssProps::new();
2779 props.insert("transition-property".into(), json!("all"));
2780 props.insert("transition-duration".into(), json!("150ms"));
2781 props.insert("transition-timing-function".into(), json!("ease-in-out"));
2782 return Some(props);
2783 }
2784 if class == "transition-none" {
2785 let mut props = CssProps::new();
2786 props.insert("transition-property".into(), json!("none"));
2787 props.insert("transition-duration".into(), json!("0ms"));
2788 return Some(props);
2789 }
2790 if let Some(rest) = class.strip_prefix("transition-") {
2791 let mut props = CssProps::new();
2793 let property = match rest {
2794 "colors" => "color, background-color, border-color, fill, stroke",
2795 "opacity" => "opacity",
2796 "transform" => "transform",
2797 "shadow" => "box-shadow",
2798 other => other,
2799 };
2800 props.insert("transition-property".into(), json!(property));
2801 props.insert("transition-duration".into(), json!("150ms"));
2802 props.insert("transition-timing-function".into(), json!("ease-in-out"));
2803 return Some(props);
2804 }
2805 if let Some(val) = class.strip_prefix("w-") {
2807 return width_like_props("width", val);
2808 }
2809 if let Some(val) = class.strip_prefix("min-w-") {
2810 return width_like_props("min-width", val);
2811 }
2812 if let Some(val) = class.strip_prefix("max-w-") {
2813 return width_like_props("max-width", val);
2814 }
2815 if let Some(val) = class.strip_prefix("h-") {
2817 return width_like_props("height", val);
2818 }
2819 if let Some(val) = class.strip_prefix("min-h-") {
2820 return width_like_props("min-height", val);
2821 }
2822 if let Some(val) = class.strip_prefix("max-h-") {
2823 return width_like_props("max-height", val);
2824 }
2825 None
2826}
2827
2828fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
2829where
2830 F: Fn(i32) -> CssProps,
2831{
2832 if let Ok(n) = value.parse::<i32>() {
2833 let px = n * 4;
2834 return Some(builder(px));
2835 }
2836 None
2837}
2838
2839fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
2840 let mut props = CssProps::new();
2841 let val = format!("{}px", px_value);
2842 for key in keys {
2843 props.insert((*key).into(), json!(&val));
2844 }
2845 props
2846}
2847
2848fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
2849 let mut props = CssProps::new();
2850 let val = format!("{}px", px_value);
2851 for key in keys {
2852 props.insert((*key).into(), json!(&val));
2853 }
2854 props
2855}
2856
2857fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
2858 let mut props = CssProps::new();
2859 let width_str = format!("{}px", width);
2860 match side {
2861 None => {
2862 props.insert("border-width".into(), json!(&width_str));
2863 }
2864 Some("t") => {
2865 props.insert("border-top-width".into(), json!(&width_str));
2866 }
2867 Some("b") => {
2868 props.insert("border-bottom-width".into(), json!(&width_str));
2869 }
2870 Some("l") => {
2871 props.insert("border-left-width".into(), json!(&width_str));
2872 }
2873 Some("r") => {
2874 props.insert("border-right-width".into(), json!(&width_str));
2875 }
2876 Some("x") => {
2877 props.insert("border-left-width".into(), json!(&width_str));
2878 props.insert("border-right-width".into(), json!(&width_str));
2879 }
2880 Some("y") => {
2881 props.insert("border-top-width".into(), json!(&width_str));
2882 props.insert("border-bottom-width".into(), json!(&width_str));
2883 }
2884 _ => {
2885 props.insert("border-width".into(), json!(&width_str));
2886 }
2887 };
2888 props.insert("border-color".into(), json!("var(border)"));
2889 props.insert("border-style".into(), json!("solid"));
2890 props
2891}
2892
2893fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
2894 let mut props = CssProps::new();
2895 let px = match size.unwrap_or("md") {
2896 "none" => 0,
2897 "sm" => 2,
2898 "md" => 4,
2899 "lg" => 8,
2900 "xl" => 12,
2901 "2xl" => 16,
2902 "3xl" => 24,
2903 "full" => 9999,
2904 s => s.parse::<i32>().unwrap_or(4),
2905 };
2906 let v = json!(format!("{}px", px));
2907 match side {
2908 None => {
2909 props.insert("border-radius".into(), v);
2910 }
2911 Some("t") => {
2912 props.insert("border-top-left-radius".into(), v.clone());
2913 props.insert("border-top-right-radius".into(), v);
2914 }
2915 Some("b") => {
2916 props.insert("border-bottom-left-radius".into(), v.clone());
2917 props.insert("border-bottom-right-radius".into(), v);
2918 }
2919 Some("l") => {
2920 props.insert("border-top-left-radius".into(), v.clone());
2921 props.insert("border-bottom-left-radius".into(), v);
2922 }
2923 Some("r") => {
2924 props.insert("border-top-right-radius".into(), v.clone());
2925 props.insert("border-bottom-right-radius".into(), v);
2926 }
2927 _ => {
2928 props.insert("border-radius".into(), v);
2929 }
2930 }
2931 props
2932}
2933
2934fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
2935 let mut props = CssProps::new();
2936 let value = match token {
2937 "full" => Some("100%".to_string()),
2938 "screen" => Some(if prop == "width" { "100vw" } else { "100vh" }.to_string()),
2939 "min" => Some("min-content".to_string()),
2940 "max" => Some("max-content".to_string()),
2941 "fit" => Some("fit-content".to_string()),
2942 "auto" => Some("auto".to_string()),
2943 "px" => Some("1px".to_string()),
2944 other => {
2945 if let Some((a, b)) = other.split_once('/') {
2947 if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
2948 let pct = (na / nb) * 100.0;
2949 Some(format!("{}%", trim_trailing_zeros(pct)))
2950 } else {
2951 None
2952 }
2953 } else if let Ok(n) = other.parse::<i32>() {
2954 Some(format!("{}px", n * 4))
2955 } else {
2956 None
2957 }
2958 }
2959 }?;
2960 props.insert(prop.into(), json!(value));
2961 Some(props)
2962}
2963
2964fn trim_trailing_zeros(num: f64) -> String {
2965 let mut s = format!("{:.6}", num);
2966 while s.contains('.') && s.ends_with('0') {
2967 s.pop();
2968 }
2969 if s.ends_with('.') {
2970 s.pop();
2971 }
2972 s
2973}
2974
2975fn css_escape_class(class: &str) -> String {
2980 class.replace(':', "\\:")
2981}
2982
2983fn class_to_selector(class: &str) -> String {
2984 let (_bp, hover, base) = parse_prefixed_class(class);
2985 if hover {
2986 format!(".{}:hover", css_escape_class(&base))
2987 } else {
2988 format!(".{}", css_escape_class(&base))
2989 }
2990}
2991
2992pub fn post_process_css(
2999 raw_rules: &[(String, CssProps)],
3000 vars: &IndexMap<String, String>,
3001) -> String {
3002 let mut normal = vec![];
3004 let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
3005 for (sel, props) in raw_rules.iter() {
3006 if let Some((media, inner)) = sel.split_once('{') {
3007 if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
3008 let inner_sel = inner.trim_end_matches('}').to_string();
3009 media_map
3010 .entry(media.trim().to_string())
3011 .or_default()
3012 .push((inner_sel, props.clone()));
3013 continue;
3014 }
3015 }
3016 normal.push((sel.clone(), props.clone()));
3017 }
3018 let mut out = String::new();
3019 for (sel, props) in normal {
3020 out.push_str(&sel);
3021 out.push('{');
3022 out.push_str(&css_props_string(&props, vars));
3023 out.push_str("}\n");
3024 }
3025 for (media, entries) in media_map {
3026 out.push_str(&media);
3027 out.push('{');
3028 for (sel, props) in entries {
3029 out.push_str(&sel);
3030 out.push('{');
3031 out.push_str(&css_props_string(&props, vars));
3032 out.push_str("}");
3033 }
3034 out.push_str("}\n");
3035 }
3036 out
3037}
3038
3039fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
3042 let parts: Vec<&str> = class.split(':').collect();
3044 if parts.len() == 1 {
3045 return (None, false, class.to_string());
3046 }
3047 let mut bp: Option<String> = None;
3048 let mut hover = false;
3049 for &p in &parts[..parts.len() - 1] {
3050 match p {
3051 "hover" => hover = true,
3052 "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
3053 _ => {}
3054 }
3055 }
3056 let base = parts.last().unwrap().to_string();
3057 (bp, hover, base)
3058}
3059
3060fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
3061 if let Some(k) = bp_key {
3062 if let Some(val) = bps.get(k) {
3063 return format!("@media (min-width: {}) {{{}}}", val, selector);
3064 }
3065 }
3066 selector.to_string()
3067}
3068
3069fn get_tailwind_color(color_shade: &str) -> Option<String> {
3071 let parts: Vec<&str> = color_shade.split('-').collect();
3072 if parts.len() != 2 {
3073 return None;
3074 }
3075 let color_name = parts[0];
3076 let shade = parts[1];
3077
3078 if let Some(hex) = TAILWIND_COLORS
3080 .get(color_name)
3081 .and_then(|shades| shades.get(shade))
3082 {
3083 return Some(hex.to_string());
3084 }
3085
3086 None
3087}
3088
3089fn get_tailwind_color_with_vars(
3090 color_shade: &str,
3091 vars: &IndexMap<String, String>,
3092) -> Option<String> {
3093 if let Some(hex) = get_tailwind_color(color_shade) {
3095 return Some(hex);
3096 }
3097
3098 if let Some(val) = vars.get(color_shade) {
3107 return Some(val.clone());
3108 }
3109
3110 if let Some(val) = vars.get(&format!("colors.{}", color_shade)) {
3112 return Some(val.clone());
3113 }
3114
3115 if let Some(val) = vars.get(&format!("color.{}", color_shade)) {
3117 return Some(val.clone());
3118 }
3119
3120 let parts: Vec<&str> = color_shade.split('-').collect();
3123 if parts.len() >= 1 {
3124 let color_name = parts[0];
3125
3126 if let Some(val) = vars.get(color_name) {
3128 return Some(val.clone());
3129 }
3130
3131 if let Some(val) = vars.get(&format!("color.{}", color_name)) {
3133 return Some(val.clone());
3134 }
3135 }
3136
3137 None
3138}
3139
3140fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
3142 if let Some(bracket_start) = class.find('[') {
3144 if !class.ends_with(']') {
3145 return None;
3146 }
3147 let prefix = &class[..bracket_start];
3148 let value = &class[bracket_start + 1..class.len() - 1];
3149
3150 let mut props = CssProps::new();
3151 match prefix {
3152 "bg" => {
3153 props.insert("background-color".into(), json!(value));
3154 return Some(props);
3155 }
3156 "text" => {
3157 props.insert("color".into(), json!(value));
3158 return Some(props);
3159 }
3160 "border" => {
3161 props.insert("border-color".into(), json!(value));
3162 return Some(props);
3163 }
3164 "divide" => {
3165 props.insert("border-color".into(), json!(value));
3166 return Some(props);
3167 }
3168 _ => return None,
3169 }
3170 }
3171 None
3172}
3173
3174pub mod api {
3176 pub use super::{SelectorStyles, State};
3177}
3178
3179#[cfg(test)]
3180mod tests {
3181 use super::*;
3182
3183 #[test]
3184 fn default_theme_has_p2() {
3185 let mut st = State::new_default();
3186 st.register_tailwind_classes(["p-2".to_string()]);
3187 let css = st.css_for_web();
3188 assert!(css.contains(".p-2{"));
3189 assert!(css.contains("padding:8px"));
3190 }
3191
3192 #[test]
3193 fn android_conversion() {
3194 let mut st = State::new_default();
3195 let mut styles = IndexMap::new();
3197 let mut button_props = IndexMap::new();
3198 button_props.insert("backgroundColor".to_string(), json!("#007bff"));
3199 styles.insert("button".to_string(), button_props);
3200 st.add_theme("default", styles);
3201 st.set_theme("default").ok();
3202
3203 let out = st.android_styles_for("button", &[]);
3204 assert!(out.get("backgroundColor").is_some());
3205 }
3206
3207 #[test]
3208 fn embedded_defaults_and_version() {
3209 let mut st = State::default_state();
3211 st.add_theme("default", IndexMap::new());
3212 st.set_theme("default").ok();
3213
3214 let mut vars = IndexMap::new();
3215 vars.insert("primary".to_string(), "#007bff".to_string());
3216 st.set_variables(vars);
3217
3218 assert!(st.themes.contains_key("default"));
3219 let def = st.themes.get("default").unwrap();
3220 assert!(def.variables.contains_key("primary"));
3221
3222 #[cfg(target_arch = "wasm32")]
3225 {
3226 let v = get_version();
3227 assert!(!v.is_empty());
3228 }
3229 }
3230
3231 #[test]
3232 fn border_color_with_direction() {
3233 let mut st = State::new_default();
3234
3235 st.register_tailwind_classes(["border-b-blue-500".to_string()]);
3237 let css = st.css_for_web();
3238 assert!(css.contains(".border-b-blue-500{"));
3239 assert!(
3240 css.contains("border-bottom-color:#3b82f6") || css.contains("border-b-color:#3b82f6")
3241 );
3242
3243 st.register_tailwind_classes(["border-t-red-500".to_string()]);
3245 let css = st.css_for_web();
3246 assert!(css.contains(".border-t-red-500{"));
3247
3248 st.register_tailwind_classes(["border-blue-500".to_string()]);
3250 let css = st.css_for_web();
3251 assert!(css.contains(".border-blue-500{"));
3252 assert!(css.contains("border-color:#3b82f6"));
3253 }
3254
3255 #[test]
3256 fn multiple_selectors_support() {
3257 let mut st = State::new_default();
3258 let mut selectors = SelectorStyles::new();
3259 let mut props = CssProps::new();
3260 props.insert("color".to_string(), serde_json::json!("#ff0000"));
3261 selectors.insert("h1, h2, h3".to_string(), props);
3262
3263 st.add_theme("test", selectors);
3264 st.set_theme("test").ok();
3265
3266 let android = st.android_styles_for("h1", &[]);
3268 assert_eq!(
3269 android.get("color").and_then(|v| v.as_str()),
3270 Some("#ff0000"),
3271 "h1 should have red color"
3272 );
3273
3274 let android = st.android_styles_for("h2", &[]);
3276 assert_eq!(
3277 android.get("color").and_then(|v| v.as_str()),
3278 Some("#ff0000"),
3279 "h2 should have red color"
3280 );
3281
3282 let android = st.android_styles_for("h3", &[]);
3284 assert_eq!(
3285 android.get("color").and_then(|v| v.as_str()),
3286 Some("#ff0000"),
3287 "h3 should have red color"
3288 );
3289 }
3290
3291 #[test]
3292 fn multiple_selectors_classes() {
3293 let mut st = State::new_default();
3294 let mut selectors = SelectorStyles::new();
3295 let mut props = CssProps::new();
3296 props.insert("padding".to_string(), serde_json::json!("10px"));
3297 selectors.insert(".btn, .link".to_string(), props);
3298
3299 st.add_theme("test", selectors);
3300 st.set_theme("test").ok();
3301
3302 let android = st.android_styles_for("div", &["btn".to_string()]);
3304 assert_eq!(
3305 android.get("padding").and_then(|v| v.as_f64()),
3306 Some(10.0),
3307 ".btn should have 10px padding"
3308 );
3309
3310 let android = st.android_styles_for("div", &["link".to_string()]);
3312 assert_eq!(
3313 android.get("padding").and_then(|v| v.as_f64()),
3314 Some(10.0),
3315 ".link should have 10px padding"
3316 );
3317 }
3318
3319 #[test]
3320 fn border_width_with_direction() {
3321 let mut st = State::new_default();
3322
3323 st.register_tailwind_classes(["border-b-2".to_string()]);
3325 let css = st.css_for_web();
3326 assert!(css.contains(".border-b-2{"));
3327 assert!(css.contains("border-bottom-width:2px"));
3328
3329 st.register_tailwind_classes(["border-2".to_string()]);
3331 let css = st.css_for_web();
3332 assert!(css.contains(".border-2{"));
3333 assert!(css.contains("border-width:2px"));
3334 }
3335
3336 #[test]
3337 fn display_flex_hover_breakpoint() {
3338 let mut st = State::new_default();
3339
3340 st.add_theme("default", IndexMap::new());
3342 st.set_theme("default").ok();
3343
3344 let mut breakpoints = IndexMap::new();
3345 breakpoints.insert("md".to_string(), "768px".to_string());
3346 st.set_breakpoints(breakpoints);
3347
3348 st.register_tailwind_classes([
3349 "block".into(),
3350 "inline-flex".into(),
3351 "hidden".into(),
3352 "md:flex".into(),
3353 "md:hover:block".into(),
3354 ]);
3355 let css = st.css_for_web();
3356 assert!(css.contains(".block{"));
3357 assert!(css.contains("display:block"));
3358 assert!(css.contains(".inline-flex{"));
3359 assert!(css.contains("display:inline-flex"));
3360 assert!(css.contains(".hidden{"));
3361 assert!(css.contains("display:none"));
3362 assert!(css.contains("@media (min-width: 768px)"));
3364 assert!(css.contains(".flex{display:flex"));
3365 assert!(css.contains(":hover{display:block"));
3367
3368 let android = st.android_styles_for("div", &["md:flex".into()]);
3370 assert_eq!(
3371 android.get("display").and_then(|v| v.as_str()),
3372 Some("flex")
3373 );
3374 }
3375
3376 #[test]
3377 fn parse_var_references_basic() {
3378 let refs = parse_var_references("var(color)");
3380 assert_eq!(refs.len(), 1);
3381 assert_eq!(refs[0].2, "color");
3382 assert_eq!(refs[0].0, 0); assert_eq!(refs[0].1, 10); let refs = parse_var_references("var(--primary)");
3387 assert_eq!(refs.len(), 1);
3388 assert_eq!(refs[0].2, "primary");
3389
3390 let refs = parse_var_references("var(--color) and var(size)");
3392 assert_eq!(refs.len(), 2);
3393 assert_eq!(refs[0].2, "color");
3394 assert_eq!(refs[1].2, "size");
3395
3396 let refs = parse_var_references("var( --spacing )");
3398 assert_eq!(refs.len(), 1);
3399 assert_eq!(refs[0].2, "spacing");
3400
3401 let refs = parse_var_references("var(color.primary-500)");
3403 assert_eq!(refs.len(), 1);
3404 assert_eq!(refs[0].2, "color.primary-500");
3405
3406 let refs = parse_var_references("no variables here");
3408 assert_eq!(refs.len(), 0);
3409
3410 let refs = parse_var_references("var(");
3412 assert_eq!(refs.len(), 0);
3413
3414 let refs = parse_var_references("var(color");
3416 assert_eq!(refs.len(), 0);
3417 }
3418
3419 #[test]
3420 fn resolve_vars_basic() {
3421 let mut vars = IndexMap::new();
3422 vars.insert("primary".to_string(), "#ff0000".to_string());
3423 vars.insert("spacing".to_string(), "8px".to_string());
3424 vars.insert("color.blue".to_string(), "#0000ff".to_string());
3425
3426 assert_eq!(resolve_vars("var(--primary)", &vars), "#ff0000");
3428 assert_eq!(resolve_vars("var(primary)", &vars), "#ff0000");
3429 assert_eq!(resolve_vars("var( --primary )", &vars), "#ff0000");
3430
3431 assert_eq!(
3433 resolve_vars("var(--primary) var(--spacing)", &vars),
3434 "#ff0000 8px"
3435 );
3436
3437 assert_eq!(resolve_vars("var(--color.blue)", &vars), "#0000ff");
3439
3440 assert_eq!(resolve_vars("var(--undefined)", &vars), "var(--undefined)");
3442
3443 assert_eq!(resolve_vars("$primary", &vars), "#ff0000");
3445
3446 assert_eq!(resolve_vars("plain text", &vars), "plain text");
3448 }
3449
3450 #[test]
3451 fn resolve_vars_edge_cases() {
3452 let mut vars = IndexMap::new();
3453 vars.insert("a".to_string(), "1".to_string());
3454 vars.insert("b".to_string(), "2".to_string());
3455
3456 assert_eq!(resolve_vars("var(a)var(b)", &vars), "12");
3458
3459 assert_eq!(
3461 resolve_vars("prefix var(a) suffix", &vars),
3462 "prefix 1 suffix"
3463 );
3464
3465 assert_eq!(resolve_vars("", &vars), "");
3467
3468 vars.insert("var123".to_string(), "value".to_string());
3470 assert_eq!(resolve_vars("var(var123)", &vars), "value");
3471
3472 vars.insert("my_var".to_string(), "test".to_string());
3474 assert_eq!(resolve_vars("var(my_var)", &vars), "test");
3475 }
3476
3477 #[test]
3478 fn test_android_scrolling_mapping() {
3479 let mut state = State::default();
3480 state.display_density = 2.0;
3481 state.scaled_density = 2.0;
3482 state.current_theme = "default".to_string();
3483
3484 let mut themes = IndexMap::new();
3485 let mut default_theme = crate::ThemeEntry::default();
3486 default_theme.name = Some("Default".to_string());
3487
3488 let mut overflow_styles = IndexMap::new();
3489 overflow_styles.insert("overflowX".to_string(), serde_json::json!("auto"));
3490 overflow_styles.insert("overflowY".to_string(), serde_json::json!("scroll"));
3491
3492 default_theme
3493 .selectors
3494 .insert(".scroller".to_string(), overflow_styles);
3495 themes.insert("default".to_string(), default_theme);
3496 state.themes = themes;
3497
3498 let styles = state.android_styles_for("div", &vec![".scroller".to_string()]);
3499
3500 assert_eq!(
3501 styles.get("androidScrollHorizontal"),
3502 Some(&serde_json::json!(true))
3503 );
3504 assert_eq!(
3505 styles.get("androidScrollVertical"),
3506 Some(&serde_json::json!(true))
3507 );
3508 }
3509
3510 #[test]
3511 fn android_flex_row_default() {
3512 let st = State::new_default();
3513 let styles = st.android_styles_for("div", &["flex".to_string()]);
3515 assert_eq!(
3516 styles.get("androidOrientation").and_then(|v| v.as_str()),
3517 Some("horizontal")
3518 );
3519 assert_eq!(
3520 styles.get("flexDirection").and_then(|v| v.as_str()),
3521 Some("row")
3522 );
3523
3524 let styles = st.android_styles_for("div", &[]);
3526 assert_eq!(
3527 styles.get("androidOrientation").and_then(|v| v.as_str()),
3528 Some("vertical")
3529 );
3530 assert_eq!(
3531 styles.get("flexDirection").and_then(|v| v.as_str()),
3532 Some("column")
3533 );
3534 }
3535
3536 #[test]
3537 fn android_gap_orientation_order() {
3538 let st = State::new_default();
3539 let styles = st.android_styles_for("div", &["flex".to_string(), "gap-4".to_string()]);
3540
3541 let keys: Vec<&String> = styles.keys().collect();
3543 let orientation_idx = keys
3544 .iter()
3545 .position(|&k| k == "androidOrientation")
3546 .unwrap();
3547 let gap_idx = keys.iter().position(|&k| k == "gap").unwrap();
3548
3549 assert!(
3550 orientation_idx < gap_idx,
3551 "androidOrientation should come before gap for correct layout processing"
3552 );
3553 }
3554
3555 #[test]
3556 fn margin_auto_support() {
3557 let mut st = State::new_default();
3558 st.register_tailwind_classes([
3559 "ml-auto".to_string(),
3560 "mr-auto".to_string(),
3561 "mx-auto".to_string(),
3562 ]);
3563
3564 let css = st.css_for_web();
3566 assert!(css.contains("margin-left:auto"));
3567 assert!(css.contains("margin-right:auto"));
3568
3569 let styles = st.android_styles_for("div", &["ml-auto".to_string()]);
3571 assert_eq!(
3572 styles.get("marginLeft").and_then(|v| v.as_str()),
3573 Some("auto")
3574 );
3575
3576 let styles = st.android_styles_for("div", &["mx-auto".to_string()]);
3577 assert_eq!(
3578 styles.get("marginLeft").and_then(|v| v.as_str()),
3579 Some("auto")
3580 );
3581 assert_eq!(
3582 styles.get("marginRight").and_then(|v| v.as_str()),
3583 Some("auto")
3584 );
3585 }
3586
3587 #[test]
3588 fn alignment_mapping() {
3589 let st = State::new_default();
3590
3591 let row_styles = st.android_styles_for(
3593 "div",
3594 &[
3595 "flex".to_string(),
3596 "justify-center".to_string(),
3597 "items-center".to_string(),
3598 ],
3599 );
3600 assert_eq!(
3601 row_styles
3602 .get("androidOrientation")
3603 .and_then(|v| v.as_str()),
3604 Some("horizontal")
3605 );
3606 assert_eq!(
3608 row_styles.get("androidGravity").and_then(|v| v.as_str()),
3609 Some("center")
3610 );
3611
3612 let col_styles = st.android_styles_for(
3614 "div",
3615 &[
3616 "flex".to_string(),
3617 "flex-col".to_string(),
3618 "justify-center".to_string(),
3619 "items-center".to_string(),
3620 ],
3621 );
3622 assert_eq!(
3623 col_styles
3624 .get("androidOrientation")
3625 .and_then(|v| v.as_str()),
3626 Some("vertical")
3627 );
3628 assert_eq!(
3630 col_styles.get("androidGravity").and_then(|v| v.as_str()),
3631 Some("center")
3632 );
3633
3634 let row_start_styles = st.android_styles_for(
3636 "div",
3637 &[
3638 "flex".to_string(),
3639 "justify-start".to_string(),
3640 "items-end".to_string(),
3641 ],
3642 );
3643 assert_eq!(
3644 row_start_styles
3645 .get("androidGravity")
3646 .and_then(|v| v.as_str()),
3647 Some("bottom|start")
3648 );
3649 }
3650
3651 #[test]
3652 fn test_button_bg_override() {
3653 let mut themes = IndexMap::new();
3654
3655 let mut variables = IndexMap::new();
3656 variables.insert("color.bg".to_string(), "#ffffff".to_string());
3657
3658 let mut selectors = IndexMap::new();
3659 let mut button_props = IndexMap::new();
3660 button_props.insert("background-color".to_string(), json!("#2563eb"));
3661 selectors.insert("button".to_string(), button_props);
3662
3663 let default_theme = ThemeEntry {
3664 name: Some("default".to_string()),
3665 inherits: None,
3666 selectors,
3667 variables,
3668 breakpoints: IndexMap::new(),
3669 };
3670
3671 themes.insert("default".to_string(), default_theme);
3672
3673 let mut state = State::new_default();
3674 state.themes = themes;
3675 state.current_theme = "default".to_string();
3676
3677 let classes = vec!["bg-bg".to_string(), "p-4".to_string()];
3679 let styles = state.android_styles_for("button", &classes);
3680
3681 println!("[test_button_bg_override] styles: {:?}", styles);
3682
3683 assert_eq!(
3685 styles
3686 .get("backgroundColor")
3687 .and_then(|v: &serde_json::Value| v.as_str()),
3688 Some("#ffffff")
3689 );
3690
3691 assert_eq!(styles.get("paddingTop"), Some(&serde_json::json!(16)));
3694 assert_eq!(styles.get("paddingVertical"), Some(&serde_json::json!(16)));
3695 }
3696
3697 #[test]
3698 fn test_class_selector_matching() {
3699 let mut themes = IndexMap::new();
3700 let mut selectors = IndexMap::new();
3701
3702 let mut bg_primary = IndexMap::new();
3703 bg_primary.insert("background-color".to_string(), json!("#3b82f6"));
3704 selectors.insert(".bg-primary".to_string(), bg_primary);
3705
3706 let default_theme = ThemeEntry {
3707 name: Some("default".to_string()),
3708 inherits: None,
3709 selectors,
3710 variables: IndexMap::new(),
3711 breakpoints: IndexMap::new(),
3712 };
3713
3714 themes.insert("default".to_string(), default_theme);
3715
3716 let mut state = State::new_default();
3717 state.themes = themes;
3718 state.current_theme = "default".to_string();
3719
3720 let classes = vec!["bg-primary".to_string()];
3721 let styles = state.android_styles_for("div", &classes);
3722
3723 assert_eq!(
3724 styles.get("backgroundColor").and_then(|v| v.as_str()),
3725 Some("#3b82f6")
3726 );
3727 }
3728
3729 #[test]
3730 fn test_css_kebab_case_conversion() {
3731 let mut themes = IndexMap::new();
3732 let mut selectors = IndexMap::new();
3733
3734 let mut props = IndexMap::new();
3735 props.insert("backgroundColor".to_string(), json!("#ffffff"));
3737 props.insert("borderTopWidth".to_string(), json!(1));
3738 selectors.insert("body".to_string(), props);
3739
3740 let default_theme = ThemeEntry {
3741 name: Some("default".to_string()),
3742 inherits: None,
3743 selectors,
3744 variables: IndexMap::new(),
3745 breakpoints: IndexMap::new(),
3746 };
3747
3748 themes.insert("default".to_string(), default_theme);
3749
3750 let mut state = State::new_default();
3751 state.themes = themes;
3752 state.current_theme = "default".to_string();
3753
3754 state.used_tags.insert("body".to_string());
3756
3757 let css = state.css_for_web();
3758 println!("[test_css_kebab_case_conversion] css: {}", css);
3759
3760 assert!(css.contains("background-color:#ffffff;"));
3761 assert!(css.contains("border-top-width:1;"));
3762 assert!(!css.contains("backgroundColor:"));
3763 assert!(!css.contains("borderTopWidth:"));
3764 }
3765}
3766
3767#[cfg(all(target_os = "android", feature = "android"))]
3768#[cfg(feature = "android")]
3769mod android_jni;
3770
3771mod bridge_common;
3772mod ffi;
3773mod utils;
3774
3775pub use ffi::*;
3776
3777pub fn build_state_from_theme_json(json: &str) -> State {
3780 bridge_common::build_state_from_theme_json(json)
3781}
3782
3783#[cfg(target_vendor = "apple")]
3784mod ios_ffi;