1use indexmap::{IndexMap, IndexSet};
2use once_cell::sync::Lazy;
3use serde::{de::Deserializer, Deserialize, Serialize};
4use serde_json::json;
5#[cfg(target_arch = "wasm32")]
6use wasm_bindgen::prelude::*;
7mod default_state;
8use default_state::bundled_state;
9
10fn default_display_density() -> f32 { 1.0 }
12fn default_scaled_density() -> f32 { 1.0 }
13
14pub type CssProps = IndexMap<String, serde_json::Value>;
15pub type SelectorStyles = IndexMap<String, CssProps>; fn dp_to_px(dp: f32, density: f32) -> i32 {
19 (dp * density).round() as i32
20}
21
22fn sp_to_px(sp: f32, scaled_density: f32) -> f32 {
24 sp * scaled_density
25}
26
27fn parse_and_convert_to_px(value: &serde_json::Value, density: f32) -> Option<serde_json::Value> {
29 match value {
30 serde_json::Value::Number(n) => {
31 let dp = n.as_f64()? as f32;
33 Some(serde_json::json!(dp_to_px(dp, density)))
34 }
35 serde_json::Value::String(s) => {
36 let trimmed = s.trim();
38 if trimmed.ends_with("px") {
39 let px = trimmed.trim_end_matches("px").trim().parse::<f32>().ok()?;
41 Some(serde_json::json!(px as i32))
42 } else if trimmed.ends_with("dp") {
43 let dp = trimmed.trim_end_matches("dp").trim().parse::<f32>().ok()?;
44 Some(serde_json::json!(dp_to_px(dp, density)))
45 } else if let Ok(num) = trimmed.parse::<f32>() {
46 Some(serde_json::json!(dp_to_px(num, density)))
48 } else {
49 None
51 }
52 }
53 _ => None
54 }
55}
56
57fn deserialize_variables<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
58where
59 D: Deserializer<'de>,
60{
61 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
62 let mut out: IndexMap<String, String> = IndexMap::new();
63 if let Some(v) = value {
64 flatten_variables(None, &v, &mut out);
65 }
66 Ok(out)
67}
68
69fn flatten_variables(prefix: Option<&str>, value: &serde_json::Value, out: &mut IndexMap<String, String>) {
70 match value {
71 serde_json::Value::Object(map) => {
72 for (k, v) in map {
73 let key = if let Some(p) = prefix {
74 format!("{}.{}", p, k)
75 } else {
76 k.to_string()
77 };
78 flatten_variables(Some(&key), v, out);
79 }
80 }
81 serde_json::Value::Array(arr) => {
82 for (idx, v) in arr.iter().enumerate() {
83 let key = if let Some(p) = prefix {
84 format!("{}.{}", p, idx)
85 } else {
86 idx.to_string()
87 };
88 flatten_variables(Some(&key), v, out);
89 }
90 }
91 serde_json::Value::Null => {}
92 serde_json::Value::Bool(b) => {
93 if let Some(p) = prefix {
94 out.insert(p.to_string(), b.to_string());
95 }
96 }
97 serde_json::Value::Number(n) => {
98 if let Some(p) = prefix {
99 out.insert(p.to_string(), n.to_string());
100 }
101 }
102 serde_json::Value::String(s) => {
103 if let Some(p) = prefix {
104 out.insert(p.to_string(), s.clone());
105 }
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Default)]
111pub struct ThemeEntry {
112 #[serde(default)]
113 pub name: Option<String>,
114 #[serde(default)]
115 pub inherits: Option<String>,
116 #[serde(default)]
117 pub selectors: SelectorStyles,
118 #[serde(default, deserialize_with = "deserialize_variables")]
119 pub variables: IndexMap<String, String>,
120 #[serde(default)]
121 pub breakpoints: IndexMap<String, String>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
125pub struct State {
126 pub themes: IndexMap<String, ThemeEntry>,
128 pub default_theme: String,
129 pub current_theme: String,
130 #[serde(default = "default_display_density")]
132 pub display_density: f32, #[serde(default = "default_scaled_density")]
134 pub scaled_density: f32, #[serde(default)]
137 pub theme_variables: IndexMap<String, IndexMap<String, String>>, #[serde(default)]
139 pub variables: IndexMap<String, String>, #[serde(default)]
141 pub breakpoints: IndexMap<String, String>, #[serde(default)]
143 pub used_selectors: IndexSet<String>, #[serde(default)]
145 pub used_classes: IndexSet<String>, #[serde(default)]
147 pub used_tags: IndexSet<String>, #[serde(default)]
150 pub used_tag_classes: IndexSet<String>,
151}
152
153#[derive(thiserror::Error, Debug)]
154pub enum Error {
155 #[error("theme not found: {0}")]
156 ThemeNotFound(String),
157}
158
159impl State {
160 pub fn new_default() -> Self {
161 return bundled_state();
163 }
164
165 pub fn default_state() -> Self {
167 bundled_state()
168 }
169
170 pub fn set_theme(&mut self, theme: impl Into<String>) -> Result<(), Error> {
171 let name = theme.into();
172 if !self.themes.contains_key(&name) {
173 return Err(Error::ThemeNotFound(name));
174 }
175 self.current_theme = name;
176 Ok(())
177 }
178
179 pub fn add_theme(&mut self, name: impl Into<String>, styles: SelectorStyles) {
180 let name = name.into();
181 let entry = self.themes.entry(name).or_default();
182 for (sel, props) in styles.into_iter() {
183 let e = entry.selectors.entry(sel).or_default();
184 merge_props(e, &props);
185 }
186 }
187
188 pub fn set_variables(&mut self, vars: IndexMap<String, String>) {
189 let cur = self.current_theme.clone();
191 let entry = self.themes.entry(cur).or_default();
192 entry.variables = vars;
193 }
194
195 pub fn set_breakpoints(&mut self, map: IndexMap<String, String>) {
196 let cur = self.current_theme.clone();
197 let entry = self.themes.entry(cur).or_default();
198 entry.breakpoints = map;
199 }
200
201 pub fn set_default_theme(&mut self, name: impl Into<String>) {
202 self.default_theme = name.into();
203 }
204
205 pub fn register_selectors<I: IntoIterator<Item = String>>(&mut self, selectors: I) {
206 for s in selectors {
207 self.used_selectors.insert(s);
208 }
209 }
210
211 pub fn register_tailwind_classes<I: IntoIterator<Item = String>>(&mut self, classes: I) {
212 for c in classes {
213 self.used_classes.insert(c);
214 }
215 }
216
217 pub fn register_tags<I: IntoIterator<Item = String>>(&mut self, tags: I) {
218 for t in tags {
219 self.used_tags.insert(t);
220 }
221 }
222
223 pub fn register_tag_class(&mut self, tag: impl Into<String>, class_: impl Into<String>) {
224 let key = format!("{}|{}", tag.into(), class_.into());
225 self.used_tag_classes.insert(key);
226 }
227
228
229 pub fn clear_usage(&mut self) {
230 self.used_selectors.clear();
231 self.used_classes.clear();
232 self.used_tags.clear();
233 self.used_tag_classes.clear();
234 }
235
236 pub fn to_json(&self) -> serde_json::Value {
237 json!({
238 "themes": self.themes,
239 "default_theme": self.default_theme,
240 "current_theme": self.current_theme,
241 "theme_variables": self.theme_variables,
243 "variables": self.variables,
244 "breakpoints": self.breakpoints,
245 "used_selectors": self.used_selectors,
246 "used_classes": self.used_classes,
247 "used_tags": self.used_tags,
248 "used_tag_classes": self.used_tag_classes,
249 })
250 }
251
252 pub fn from_json(value: serde_json::Value) -> anyhow::Result<Self> {
253 let state: State = serde_json::from_value(value)?;
254 Ok(state)
255 }
256
257 pub fn css_for_web(&self) -> String {
258 let (eff, vars) = self.effective_theme_all();
260 let bps = self.effective_breakpoints();
261 let mut rules: Vec<(String, CssProps)> = Vec::new();
262
263 let mut used_tags: IndexSet<String> = self.used_tags.clone();
265 let mut used_classes: IndexSet<String> = self.used_classes.clone();
266 for key in &self.used_tag_classes {
267 if let Some((t, c)) = split_tag_class_key(key) {
268 used_tags.insert(t);
269 used_classes.insert(c);
270 }
271 }
272
273 for (sel, props) in eff.iter() {
279 if should_emit_selector(sel, &used_tags, &used_classes, &self.used_tag_classes) {
280 rules.push((sel.clone(), props.clone()));
281 }
282 }
283
284 for class in &used_classes {
286 let (bp_key, hover, base) = parse_prefixed_class(class);
287 let selector = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
288
289 if let Some(props) = eff.get(&selector) {
291 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
292 rules.push((final_sel, props.clone()));
293 continue;
294 }
295 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
297 let sel = if hover { format!(".{}:hover", css_escape_class(&base)) } else { format!(".{}", css_escape_class(&base)) };
298 let final_sel = wrap_with_media(&sel, bp_key.as_deref(), &bps);
299 rules.push((final_sel, dynamic_props));
300 continue;
301 }
302 if let Some(props) = eff.get(&base) {
304 let final_sel = wrap_with_media(&selector, bp_key.as_deref(), &bps);
305 rules.push((final_sel, props.clone()));
306 }
307 }
308
309 post_process_css(&rules, &vars)
310 }
311
312 pub fn rn_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
313 let (eff, vars) = self.effective_theme_all();
314 let mut out: IndexMap<String, serde_json::Value> = IndexMap::new();
315 if let Some(props) = eff.get(selector) {
316 merge_rn_props(&mut out, props, &vars);
317 }
318 for class in classes {
319 let normalized_class = if class.starts_with('.') {
321 class[1..].to_string()
322 } else {
323 class.clone()
324 };
325
326 let (_bp, _hover, base) = parse_prefixed_class(&normalized_class);
327 let sel = class_to_selector(&base);
329 if let Some(props) = eff.get(&sel) {
330 merge_rn_props(&mut out, props, &vars);
331 continue;
332 }
333 if let Some(dynamic_props) = dynamic_css_properties_for_class(&base, &vars) {
335 merge_rn_props(&mut out, &dynamic_props, &vars);
336 continue;
337 }
338 if let Some(props) = eff.get(&base) {
339 merge_rn_props(&mut out, props, &vars);
340 }
341 }
342
343 if let Some(display) = out.get("display") {
345 eprintln!("[CSS Semantics] Found display={:?}, has flex-direction={}", display, out.contains_key("flex-direction"));
346 if display.as_str() == Some("flex") && !out.contains_key("flex-direction") {
347 eprintln!("[CSS Semantics] Adding default flex-direction: row");
348 out.insert("flex-direction".to_string(), serde_json::json!("row"));
349 }
350 }
351
352 out
353 }
354
355 pub fn android_styles_for(&self, selector: &str, classes: &[String]) -> IndexMap<String, serde_json::Value> {
359 let mut styles = self.rn_styles_for(selector, classes);
360 let density = self.display_density;
361 let scaled_density = self.scaled_density;
362
363 let dimension_props = [
365 "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
366 "padding", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
367 "paddingHorizontal", "paddingVertical",
368 "margin", "marginTop", "marginBottom", "marginLeft", "marginRight",
369 "marginHorizontal", "marginVertical",
370 "borderRadius", "borderWidth", "borderTopWidth", "borderBottomWidth",
371 "borderLeftWidth", "borderRightWidth",
372 "gap", "rowGap", "columnGap", "elevation"
373 ];
374
375 for prop in &dimension_props {
376 if let Some(value) = styles.get(*prop).cloned() {
377 if let Some(converted) = parse_and_convert_to_px(&value, density) {
378 styles.insert(prop.to_string(), converted);
379 }
380 }
381 }
382
383 if let Some(font_size) = styles.get("fontSize").cloned() {
385 if let Some(serde_json::Value::Number(n)) = parse_and_convert_to_px(&font_size, density).as_ref() {
386 let sp_value = n.as_f64().unwrap_or(14.0) as f32 / density * scaled_density;
388 styles.insert("fontSize".to_string(), serde_json::json!(sp_value));
389 }
390 }
391
392 if selector == "div" || selector == "view" {
394 if styles.get("flex-direction").map_or(false, |v| v.as_str() == Some("row")) {
395 if !styles.contains_key("width") {
396 styles.insert("width".to_string(), serde_json::json!("match_parent"));
397 }
398 } else if !styles.contains_key("width") {
399 styles.insert("width".to_string(), serde_json::json!("match_parent"));
400 }
401 if selector == "div" && !styles.contains_key("height") {
402 styles.insert("height".to_string(), serde_json::json!("wrap_content"));
403 }
404 }
405
406 if selector == "span" || selector == "text" {
408 if !styles.contains_key("width") {
409 styles.insert("width".to_string(), serde_json::json!("wrap_content"));
410 }
411 if !styles.contains_key("height") {
412 styles.insert("height".to_string(), serde_json::json!("wrap_content"));
413 }
414 }
415
416 if let Some(flex_wrap) = styles.get("flex-wrap") {
418 if flex_wrap.as_str() == Some("wrap") {
419 styles.insert("androidFlexWrap".to_string(), serde_json::json!(true));
420 }
421 }
422
423 if let Some(align_items) = styles.get("align-items") {
425 let gravity = match align_items.as_str() {
426 Some("center") => "center_vertical",
427 Some("flex-start") | Some("start") => "top",
428 Some("flex-end") | Some("end") => "bottom",
429 _ => ""
430 };
431 if !gravity.is_empty() {
432 styles.insert("androidGravity".to_string(), serde_json::json!(gravity));
433 }
434 }
435
436 if let Some(justify) = styles.get("justify-content") {
438 let layout_gravity = match justify.as_str() {
439 Some("center") => "center_horizontal",
440 Some("flex-start") | Some("start") => "start",
441 Some("flex-end") | Some("end") => "end",
442 Some("space-between") => "space_between",
443 Some("space-around") => "space_around",
444 _ => ""
445 };
446 if !layout_gravity.is_empty() {
447 styles.insert("androidLayoutGravity".to_string(), serde_json::json!(layout_gravity));
448 }
449 }
450
451 if let Some(overflow_x) = styles.get("overflowX") {
453 if overflow_x.as_str() == Some("auto") || overflow_x.as_str() == Some("scroll") {
454 styles.insert("androidScrollHorizontal".to_string(), serde_json::json!(true));
455 }
456 }
457 if let Some(overflow_y) = styles.get("overflowY") {
458 if overflow_y.as_str() == Some("auto") || overflow_y.as_str() == Some("scroll") {
459 styles.insert("androidScrollVertical".to_string(), serde_json::json!(true));
460 }
461 }
462
463 if let Some(text_align) = styles.get("textAlign") {
465 let gravity = match text_align.as_str() {
466 Some("center") => "center_horizontal",
467 Some("right") | Some("end") => "end",
468 Some("left") | Some("start") => "start",
469 _ => ""
470 };
471 if !gravity.is_empty() {
472 styles.insert("androidTextGravity".to_string(), serde_json::json!(gravity));
473 }
474 }
475
476 if let Some(font_weight) = styles.get("fontWeight") {
478 let is_bold = match font_weight {
479 serde_json::Value::String(s) => s.contains("bold"),
480 serde_json::Value::Number(n) => {
481 let weight = n.as_i64().unwrap_or(400);
482 weight >= 500
483 }
484 _ => false
485 };
486 styles.insert("androidFontBold".to_string(), serde_json::json!(is_bold));
487 }
488
489 if let Some(box_shadow) = styles.get("boxShadow") {
491 if let Some(shadow_str) = box_shadow.as_str() {
492 if !shadow_str.is_empty() {
493 let elevation_dp = if shadow_str.contains("20px") { 24.0 }
494 else if shadow_str.contains("15px") { 16.0 }
495 else if shadow_str.contains("10px") { 8.0 }
496 else if shadow_str.contains("5px") { 4.0 }
497 else { 4.0 };
498 styles.insert("elevation".to_string(), serde_json::json!(dp_to_px(elevation_dp, density)));
499 }
500 }
501 }
502
503 styles
504 }
505
506 fn theme_chain(&self) -> Vec<String> {
510 let mut chain = Vec::new();
511 let default_name = if self.themes.contains_key(&self.default_theme) {
513 self.default_theme.clone()
514 } else if let Some((k, _)) = self.themes.first() { k.clone() } else { return chain };
515 let mut current_name = if self.themes.contains_key(&self.current_theme) {
516 self.current_theme.clone()
517 } else { default_name.clone() };
518 let mut seen: IndexSet<String> = IndexSet::new();
520 while !seen.contains(¤t_name) {
521 seen.insert(current_name.clone());
522 chain.push(current_name.clone());
523 let inherits = self.themes.get(¤t_name).and_then(|t| t.inherits.clone());
525 if let Some(p) = inherits {
526 current_name = p;
527 } else {
528 break;
529 }
530 }
531 if !chain.iter().any(|n| n == &default_name) {
532 chain.push(default_name);
533 }
534 chain
535 }
536
537 fn effective_theme_all(&self) -> (SelectorStyles, IndexMap<String, String>) {
540 let mut selectors: SelectorStyles = SelectorStyles::new();
541 let mut vars: IndexMap<String, String> = IndexMap::new();
542 for (k, v) in self.variables.iter() { vars.insert(k.clone(), v.clone()); }
544 let chain = self.theme_chain();
546 for name in chain.into_iter().rev() {
547 if let Some(entry) = self.themes.get(&name) {
548 for (sel, props) in entry.selectors.iter() {
550 let e = selectors.entry(sel.clone()).or_default();
551 merge_props(e, props);
552 }
553 for (k, v) in entry.variables.iter() {
555 vars.insert(k.clone(), v.clone());
556 }
557 }
558 }
559 (selectors, vars)
560 }
561
562 pub fn effective_breakpoints(&self) -> IndexMap<String, String> {
564 let mut bps: IndexMap<String, String> = IndexMap::new();
565 for (k, v) in self.breakpoints.iter() { bps.insert(k.clone(), v.clone()); }
567 let chain = self.theme_chain();
568 for name in chain.into_iter().rev() {
569 if let Some(entry) = self.themes.get(&name) {
570 for (k, v) in entry.breakpoints.iter() {
571 bps.insert(k.clone(), v.clone());
572 }
573 }
574 }
575 bps
576 }
577}
578
579fn split_tag_class_key(key: &str) -> Option<(String, String)> {
580 let mut it = key.splitn(2, '|');
581 let t = it.next()?.to_string();
582 let c = it.next()?.to_string();
583 if t.is_empty() || c.is_empty() { return None; }
584 Some((t, c))
585}
586
587fn strip_hover_suffix(selector: &str) -> (&str, bool) {
588 if let Some(stripped) = selector.strip_suffix(":hover") { (stripped, true) } else { (selector, false) }
589}
590
591fn should_emit_selector(sel: &str, used_tags: &IndexSet<String>, used_classes: &IndexSet<String>, used_tag_classes: &IndexSet<String>) -> bool {
592 let (base, _hover) = strip_hover_suffix(sel);
594
595 if is_simple_tag(base) {
597 return used_tags.contains(base) || used_tag_classes.iter().any(|k| k.split('|').next() == Some(base));
598 }
599
600 if let Some(class_name) = base.strip_prefix('.') {
602 return used_classes.contains(class_name) || used_tag_classes.iter().any(|k| k.ends_with(&format!("|{}", class_name)));
604 }
605
606 if let Some((tag, class_name)) = split_tag_class_selector(base) {
608 let key = format!("{}|{}", tag, class_name);
609 return used_tag_classes.contains(&key) || (used_tags.contains(&tag) && used_classes.contains(&class_name));
610 }
611
612 false
614}
615
616fn is_simple_tag(s: &str) -> bool {
617 let mut chars = s.chars();
619 match chars.next() { Some(c) if c.is_ascii_alphabetic() => {}, _ => return false }
620 chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
621}
622
623fn split_tag_class_selector(s: &str) -> Option<(String, String)> {
624 let mut parts = s.splitn(2, '.');
626 let tag = parts.next()?.to_string();
627 let class_name = parts.next()?.to_string();
628 if tag.is_empty() || class_name.is_empty() { return None; }
629 Some((tag, class_name))
630}
631
632#[cfg(target_arch = "wasm32")]
634#[wasm_bindgen]
635pub fn render_css_for_web(state_json: &str) -> String {
636 match serde_json::from_str::<State>(state_json) {
637 Ok(s) => s.css_for_web(),
638 Err(_) => "".into(),
639 }
640}
641
642#[cfg(target_arch = "wasm32")]
643#[wasm_bindgen]
644pub fn get_rn_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
645 let classes: Vec<String> = serde_json::from_str(classes_json).unwrap_or_default();
646 match serde_json::from_str::<State>(state_json) {
647 Ok(s) => serde_json::to_string(&s.rn_styles_for(selector, &classes)).unwrap_or_else(|_| "{}".into()),
648 Err(_) => "{}".into(),
649 }
650}
651
652#[cfg(target_arch = "wasm32")]
655#[wasm_bindgen]
656pub fn get_android_styles(state_json: &str, selector: &str, classes_json: &str) -> String {
657 get_rn_styles(state_json, selector, classes_json)
658}
659
660#[cfg(target_arch = "wasm32")]
662#[wasm_bindgen]
663pub fn get_version() -> String {
664 env!("CARGO_PKG_VERSION").to_string()
666}
667
668pub fn version() -> &'static str {
670 env!("CARGO_PKG_VERSION")
671}
672
673#[cfg(target_arch = "wasm32")]
675#[wasm_bindgen]
676pub fn get_default_state_json() -> String {
677 let st = bundled_state();
678 match serde_json::to_string(&st.to_json()) {
679 Ok(s) => s,
680 Err(_) => "{}".to_string(),
681 }
682}
683
684#[cfg(target_arch = "wasm32")]
688#[wasm_bindgen]
689pub fn register_theme_json(state_json: &str, theme_json: &str) -> String {
690 match (serde_json::from_str::<State>(state_json), serde_json::from_str::<serde_json::Value>(theme_json)) {
691 (Ok(mut state), Ok(theme_obj)) => {
692 if let (Some(name), Some(theme_entry)) = (theme_obj.get("name"), theme_obj.get("theme")) {
693 if let Ok(entry) = serde_json::from_value::<ThemeEntry>(theme_entry.clone()) {
694 let theme_name = name.as_str().unwrap_or("").to_string();
695 if !theme_name.is_empty() {
696 state.themes.insert(theme_name, entry);
697 }
698 }
699 }
700 match serde_json::to_string(&state.to_json()) {
701 Ok(s) => s,
702 Err(_) => "{}".to_string(),
703 }
704 }
705 _ => "{}".to_string(),
706 }
707}
708
709#[cfg(target_arch = "wasm32")]
711#[wasm_bindgen]
712pub fn set_theme_json(state_json: &str, theme_name: &str) -> String {
713 match serde_json::from_str::<State>(state_json) {
714 Ok(mut state) => {
715 if state.themes.contains_key(theme_name) {
716 state.default_theme = theme_name.to_string();
717 state.current_theme = theme_name.to_string();
718 }
719 match serde_json::to_string(&state.to_json()) {
720 Ok(s) => s,
721 Err(_) => "{}".to_string(),
722 }
723 }
724 _ => "{}".to_string(),
725 }
726}
727
728#[cfg(target_arch = "wasm32")]
731#[wasm_bindgen]
732pub fn get_theme_list_json(state_json: &str) -> String {
733 match serde_json::from_str::<State>(state_json) {
734 Ok(state) => {
735 let themes: Vec<serde_json::Value> = state.themes.iter().map(|(key, entry)| {
736 json!({
737 "key": key,
738 "name": entry.name.as_ref().unwrap_or(key)
739 })
740 }).collect();
741 serde_json::to_string(&themes).unwrap_or_else(|_| "[]".to_string())
742 }
743 _ => "[]".to_string(),
744 }
745}
746
747fn merge_props(into: &mut CssProps, from: &CssProps) {
748 for (k, v) in from.iter() {
749 into.insert(k.clone(), v.clone());
750 }
751}
752
753fn css_props_string(props: &CssProps, vars: &IndexMap<String, String>) -> String {
756 let mut buf = String::new();
757 for (k, v) in props.iter() {
758 buf.push_str(k);
759 buf.push(':');
760 let val = if v.is_string() {
761 let s = v.as_str().unwrap();
762 resolve_vars(s, vars)
763 } else {
764 v.to_string()
765 };
766 buf.push_str(&val);
767 if !val.ends_with(';') {
768 buf.push(';');
769 }
770 }
771 buf
772}
773
774fn parse_var_references(input: &str) -> Vec<(usize, usize, String)> {
778 let mut results = Vec::new();
779 let bytes = input.as_bytes();
780 let mut i = 0;
781
782 while i < bytes.len() {
783 if i + 4 <= bytes.len() && &bytes[i..i+4] == b"var(" {
785 let start = i;
786 i += 4;
787
788 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
790 i += 1;
791 }
792
793 let has_prefix = i + 2 <= bytes.len() && &bytes[i..i+2] == b"--";
795 if has_prefix {
796 i += 2;
797 }
798
799 let name_start = i;
801 while i < bytes.len() {
802 let c = bytes[i];
803 if (c >= b'a' && c <= b'z') || (c >= b'A' && c <= b'Z') ||
804 (c >= b'0' && c <= b'9') || c == b'_' || c == b'.' || c == b'-' {
805 i += 1;
806 } else {
807 break;
808 }
809 }
810
811 let name_end = i;
812 if name_start < name_end {
813 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
815 i += 1;
816 }
817
818 if i < bytes.len() && bytes[i] == b')' {
820 let end = i + 1;
821 let var_name = std::str::from_utf8(&bytes[name_start..name_end])
822 .unwrap_or("").to_string();
823 results.push((start, end, var_name));
824 i = end;
825 continue;
826 }
827 }
828 }
829 i += 1;
830 }
831
832 results
833}
834
835static TAILWIND_COLORS: Lazy<IndexMap<&'static str, IndexMap<&'static str, &'static str>>> = Lazy::new(|| {
837 let mut colors = IndexMap::new();
838
839 let mut slate = IndexMap::new();
840 slate.insert("50", "#f8fafc"); slate.insert("100", "#f1f5f9"); slate.insert("200", "#e2e8f0");
841 slate.insert("300", "#cbd5e1"); slate.insert("400", "#94a3b8"); slate.insert("500", "#64748b");
842 slate.insert("600", "#475569"); slate.insert("700", "#334155"); slate.insert("800", "#1e293b");
843 slate.insert("900", "#0f172a"); slate.insert("950", "#020617");
844 colors.insert("slate", slate);
845
846 let mut gray = IndexMap::new();
847 gray.insert("50", "#f9fafb"); gray.insert("100", "#f3f4f6"); gray.insert("200", "#e5e7eb");
848 gray.insert("300", "#d1d5db"); gray.insert("400", "#9ca3af"); gray.insert("500", "#6b7280");
849 gray.insert("600", "#4b5563"); gray.insert("700", "#374151"); gray.insert("800", "#1f2937");
850 gray.insert("900", "#111827"); gray.insert("950", "#030712");
851 colors.insert("gray", gray);
852
853 let mut zinc = IndexMap::new();
854 zinc.insert("50", "#fafafa"); zinc.insert("100", "#f4f4f5"); zinc.insert("200", "#e4e4e7");
855 zinc.insert("300", "#d4d4d8"); zinc.insert("400", "#a1a1aa"); zinc.insert("500", "#71717a");
856 zinc.insert("600", "#52525b"); zinc.insert("700", "#3f3f46"); zinc.insert("800", "#27272a");
857 zinc.insert("900", "#18181b"); zinc.insert("950", "#09090b");
858 colors.insert("zinc", zinc);
859
860 let mut neutral = IndexMap::new();
861 neutral.insert("50", "#fafafa"); neutral.insert("100", "#f5f5f5"); neutral.insert("200", "#e5e5e5");
862 neutral.insert("300", "#d4d4d4"); neutral.insert("400", "#a3a3a3"); neutral.insert("500", "#737373");
863 neutral.insert("600", "#525252"); neutral.insert("700", "#404040"); neutral.insert("800", "#262626");
864 neutral.insert("900", "#171717"); neutral.insert("950", "#0a0a0a");
865 colors.insert("neutral", neutral);
866
867 let mut stone = IndexMap::new();
868 stone.insert("50", "#fafaf9"); stone.insert("100", "#f5f5f4"); stone.insert("200", "#e7e5e4");
869 stone.insert("300", "#d6d3d1"); stone.insert("400", "#a8a29e"); stone.insert("500", "#78716c");
870 stone.insert("600", "#57534e"); stone.insert("700", "#44403c"); stone.insert("800", "#292524");
871 stone.insert("900", "#1c1917"); stone.insert("950", "#0c0a09");
872 colors.insert("stone", stone);
873
874 let mut red = IndexMap::new();
875 red.insert("50", "#fef2f2"); red.insert("100", "#fee2e2"); red.insert("200", "#fecaca");
876 red.insert("300", "#fca5a5"); red.insert("400", "#f87171"); red.insert("500", "#ef4444");
877 red.insert("600", "#dc2626"); red.insert("700", "#b91c1c"); red.insert("800", "#991b1b");
878 red.insert("900", "#7f1d1d"); red.insert("950", "#450a0a");
879 colors.insert("red", red);
880
881 let mut orange = IndexMap::new();
882 orange.insert("50", "#fff7ed"); orange.insert("100", "#ffedd5"); orange.insert("200", "#fed7aa");
883 orange.insert("300", "#fdba74"); orange.insert("400", "#fb923c"); orange.insert("500", "#f97316");
884 orange.insert("600", "#ea580c"); orange.insert("700", "#c2410c"); orange.insert("800", "#9a3412");
885 orange.insert("900", "#7c2d12"); orange.insert("950", "#431407");
886 colors.insert("orange", orange);
887
888 let mut amber = IndexMap::new();
889 amber.insert("50", "#fffbeb"); amber.insert("100", "#fef3c7"); amber.insert("200", "#fde68a");
890 amber.insert("300", "#fcd34d"); amber.insert("400", "#fbbf24"); amber.insert("500", "#f59e0b");
891 amber.insert("600", "#d97706"); amber.insert("700", "#b45309"); amber.insert("800", "#92400e");
892 amber.insert("900", "#78350f"); amber.insert("950", "#451a03");
893 colors.insert("amber", amber);
894
895 let mut blue = IndexMap::new();
896 blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
897 blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
898 blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
899 blue.insert("900", "#1e3a8a"); blue.insert("950", "#0b1c52");
900 colors.insert("blue", blue);
901
902 let mut lime = IndexMap::new();
903 lime.insert("50", "#f7fee7"); lime.insert("100", "#ecfccb"); lime.insert("200", "#d9f99d");
904 lime.insert("300", "#bef264"); lime.insert("400", "#a3e635"); lime.insert("500", "#84cc16");
905 lime.insert("600", "#65a30d"); lime.insert("700", "#4d7c0f"); lime.insert("800", "#3f6212");
906 lime.insert("900", "#365314"); lime.insert("950", "#1a2e05");
907 colors.insert("lime", lime);
908
909 let mut green = IndexMap::new();
910 green.insert("50", "#f0fdf4"); green.insert("100", "#dcfce7"); green.insert("200", "#bbf7d0");
911 green.insert("300", "#86efac"); green.insert("400", "#4ade80"); green.insert("500", "#22c55e");
912 green.insert("600", "#16a34a"); green.insert("700", "#15803d"); green.insert("800", "#166534");
913 green.insert("900", "#14532d"); green.insert("950", "#052e16");
914 colors.insert("green", green);
915
916 let mut emerald = IndexMap::new();
917 emerald.insert("50", "#ecfdf5"); emerald.insert("100", "#d1fae5"); emerald.insert("200", "#a7f3d0");
918 emerald.insert("300", "#6ee7b7"); emerald.insert("400", "#34d399"); emerald.insert("500", "#10b981");
919 emerald.insert("600", "#059669"); emerald.insert("700", "#047857"); emerald.insert("800", "#065f46");
920 emerald.insert("900", "#064e3b"); emerald.insert("950", "#022c22");
921 colors.insert("emerald", emerald);
922
923 let mut teal = IndexMap::new();
924 teal.insert("50", "#f0fdfa"); teal.insert("100", "#ccfbf1"); teal.insert("200", "#99f6e4");
925 teal.insert("300", "#5eead4"); teal.insert("400", "#2dd4bf"); teal.insert("500", "#14b8a6");
926 teal.insert("600", "#0d9488"); teal.insert("700", "#0f766e"); teal.insert("800", "#115e59");
927 teal.insert("900", "#134e4a"); teal.insert("950", "#042f2e");
928 colors.insert("teal", teal);
929
930 let mut cyan = IndexMap::new();
931 cyan.insert("50", "#ecfeff"); cyan.insert("100", "#cffafe"); cyan.insert("200", "#a5f3fc");
932 cyan.insert("300", "#67e8f9"); cyan.insert("400", "#22d3ee"); cyan.insert("500", "#06b6d4");
933 cyan.insert("600", "#0891b2"); cyan.insert("700", "#0e7490"); cyan.insert("800", "#155e75");
934 cyan.insert("900", "#164e63"); cyan.insert("950", "#083344");
935 colors.insert("cyan", cyan);
936
937 let mut sky = IndexMap::new();
938 sky.insert("50", "#f0f9ff"); sky.insert("100", "#e0f2fe"); sky.insert("200", "#bae6fd");
939 sky.insert("300", "#7dd3fc"); sky.insert("400", "#38bdf8"); sky.insert("500", "#0ea5e9");
940 sky.insert("600", "#0284c7"); sky.insert("700", "#0369a1"); sky.insert("800", "#075985");
941 sky.insert("900", "#0c4a6e"); sky.insert("950", "#082f49");
942 colors.insert("sky", sky);
943
944 let mut blue = IndexMap::new();
945 blue.insert("50", "#eff6ff"); blue.insert("100", "#dbeafe"); blue.insert("200", "#bfdbfe");
946 blue.insert("300", "#93c5fd"); blue.insert("400", "#60a5fa"); blue.insert("500", "#3b82f6");
947 blue.insert("600", "#2563eb"); blue.insert("700", "#1d4ed8"); blue.insert("800", "#1e40af");
948 blue.insert("900", "#1e3a8a"); blue.insert("950", "#172554");
949 colors.insert("blue", blue);
950
951 let mut indigo = IndexMap::new();
952 indigo.insert("50", "#eef2ff"); indigo.insert("100", "#e0e7ff"); indigo.insert("200", "#c7d2fe");
953 indigo.insert("300", "#a5b4fc"); indigo.insert("400", "#818cf8"); indigo.insert("500", "#6366f1");
954 indigo.insert("600", "#4f46e5"); indigo.insert("700", "#4338ca"); indigo.insert("800", "#3730a3");
955 indigo.insert("900", "#312e81"); indigo.insert("950", "#1e1b4b");
956 colors.insert("indigo", indigo);
957
958 let mut violet = IndexMap::new();
959 violet.insert("50", "#f5f3ff"); violet.insert("100", "#ede9fe"); violet.insert("200", "#ddd6fe");
960 violet.insert("300", "#c4b5fd"); violet.insert("400", "#a78bfa"); violet.insert("500", "#8b5cf6");
961 violet.insert("600", "#7c3aed"); violet.insert("700", "#6d28d9"); violet.insert("800", "#5b21b6");
962 violet.insert("900", "#4c1d95"); violet.insert("950", "#2e1065");
963 colors.insert("violet", violet);
964
965 let mut purple = IndexMap::new();
966 purple.insert("50", "#faf5ff"); purple.insert("100", "#f3e8ff"); purple.insert("200", "#e9d5ff");
967 purple.insert("300", "#d8b4fe"); purple.insert("400", "#c084fc"); purple.insert("500", "#a855f7");
968 purple.insert("600", "#9333ea"); purple.insert("700", "#7e22ce"); purple.insert("800", "#6b21a8");
969 purple.insert("900", "#581c87"); purple.insert("950", "#3b0764");
970 colors.insert("purple", purple);
971
972 let mut fuchsia = IndexMap::new();
973 fuchsia.insert("50", "#fdf4ff"); fuchsia.insert("100", "#fae8ff"); fuchsia.insert("200", "#f5d0fe");
974 fuchsia.insert("300", "#f0abfc"); fuchsia.insert("400", "#e879f9"); fuchsia.insert("500", "#d946ef");
975 fuchsia.insert("600", "#c026d3"); fuchsia.insert("700", "#a21caf"); fuchsia.insert("800", "#86198f");
976 fuchsia.insert("900", "#701a75"); fuchsia.insert("950", "#4a044e");
977 colors.insert("fuchsia", fuchsia);
978
979 let mut pink = IndexMap::new();
980 pink.insert("50", "#fdf2f8"); pink.insert("100", "#fce7f3"); pink.insert("200", "#fbcfe8");
981 pink.insert("300", "#f9a8d4"); pink.insert("400", "#f472b6"); pink.insert("500", "#ec4899");
982 pink.insert("600", "#db2777"); pink.insert("700", "#be185d"); pink.insert("800", "#9d174d");
983 pink.insert("900", "#831843"); pink.insert("950", "#500724");
984 colors.insert("pink", pink);
985
986 let mut rose = IndexMap::new();
987 rose.insert("50", "#fff1f2"); rose.insert("100", "#ffe4e6"); rose.insert("200", "#fecdd3");
988 rose.insert("300", "#fda4af"); rose.insert("400", "#fb7185"); rose.insert("500", "#f43f5e");
989 rose.insert("600", "#e11d48"); rose.insert("700", "#be123c"); rose.insert("800", "#9f1239");
990 rose.insert("900", "#881337"); rose.insert("950", "#4c0519");
991 colors.insert("rose", rose);
992
993 colors
994});
995
996fn resolve_vars(input: &str, vars: &IndexMap<String, String>) -> String {
997 let var_refs = parse_var_references(input);
998
999 if var_refs.is_empty() {
1000 if input.starts_with('$') {
1002 if let Some(val) = vars.get(&input[1..]) {
1003 return val.clone();
1004 }
1005 }
1006 return input.to_string();
1007 }
1008
1009 let mut out = input.to_string();
1011 for (start, end, var_name) in var_refs.iter().rev() {
1012 if let Some(val) = vars.get(var_name) {
1013 out.replace_range(*start..*end, val);
1014 }
1015 }
1016
1017 if out.starts_with('$') {
1019 if let Some(val) = vars.get(&out[1..]) {
1020 return val.clone();
1021 }
1022 }
1023
1024 out
1025}
1026
1027fn camel_case(name: &str) -> String {
1028 let mut out = String::new();
1029 let mut upper = false;
1030 for ch in name.chars() {
1031 if ch == '-' {
1032 upper = true;
1033 continue;
1034 }
1035 if upper {
1036 out.extend(ch.to_uppercase());
1037 upper = false;
1038 } else {
1039 out.push(ch);
1040 }
1041 }
1042 out
1043}
1044
1045fn css_value_to_rn(
1046 value: &serde_json::Value,
1047 vars: &IndexMap<String, String>,
1048) -> serde_json::Value {
1049 match value {
1050 serde_json::Value::String(s) => {
1051 let s2 = resolve_vars(s, vars);
1052 if let Some(n) = s2.strip_suffix("px") {
1053 if let Ok(parsed) = n.trim().parse::<f64>() {
1054 return json!(parsed);
1055 }
1056 }
1057 json!(s2)
1058 }
1059 _ => value.clone(),
1060 }
1061}
1062
1063fn merge_rn_props(
1064 into: &mut IndexMap<String, serde_json::Value>,
1065 css_props: &CssProps,
1066 vars: &IndexMap<String, String>,
1067) {
1068 for (k, v) in css_props.iter() {
1069 let rn_key = match k.as_str() {
1070 "background-color" => "backgroundColor".to_string(),
1072 "text-align" => "textAlign".to_string(),
1073 _ => camel_case(k),
1074 };
1075 let rn_val = css_value_to_rn(v, vars);
1076 into.insert(rn_key, rn_val);
1077 }
1078}
1079
1080fn dynamic_css_properties_for_class(class: &str, vars: &IndexMap<String, String>) -> Option<CssProps> {
1081 match class {
1083 "block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("block")); return Some(p); }
1084 "inline-block" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-block")); return Some(p); }
1085 "inline" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline")); return Some(p); }
1086 "inline-flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("inline-flex")); return Some(p); }
1087 "grid" => { let mut p = CssProps::new(); p.insert("display".into(), json!("grid")); return Some(p); }
1088 "hidden" => { let mut p = CssProps::new(); p.insert("display".into(), json!("none")); return Some(p); }
1089 _ => {}
1090 }
1091 match class {
1093 "flex" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); return Some(p); }
1094 "flex-row" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("row")); return Some(p); }
1095 "flex-col" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-direction".into(), json!("column")); return Some(p); }
1096 "flex-wrap" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("wrap")); return Some(p); }
1097 "flex-nowrap" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("nowrap")); return Some(p); }
1098 "flex-wrap-reverse" => { let mut p = CssProps::new(); p.insert("display".into(), json!("flex")); p.insert("flex-wrap".into(), json!("wrap-reverse")); return Some(p); }
1099 "flex-1" => { let mut p = CssProps::new(); p.insert("flex".into(), json!(1)); return Some(p); }
1100 _ => {}
1101 }
1102 if let Some(rest) = class.strip_prefix("items-") {
1103 let mut p = CssProps::new();
1104 let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "stretch" => "stretch", other => other };
1105 p.insert("align-items".into(), json!(v));
1106 return Some(p);
1107 }
1108 if let Some(rest) = class.strip_prefix("justify-") {
1109 let mut p = CssProps::new();
1110 let v = match rest { "start" => "flex-start", "end" => "flex-end", "center" => "center", "between" => "space-between", "around" => "space-around", "evenly" => "space-evenly", other => other };
1111 p.insert("justify-content".into(), json!(v));
1112 return Some(p);
1113 }
1114 if let Some(value) = class.strip_prefix("p-") {
1115 return parse_tailwind_spacing(value, &|px| padding_props(&["padding"], px));
1116 }
1117 if let Some(value) = class.strip_prefix("px-") {
1118 return parse_tailwind_spacing(value, &|px| padding_props(&["padding-left", "padding-right"], px));
1119 }
1120 if let Some(value) = class.strip_prefix("py-") {
1121 return parse_tailwind_spacing(value, &|px| padding_props(&["padding-top", "padding-bottom"], px));
1122 }
1123 for &(prefix, prop) in &[("pt-", "padding-top"), ("pr-", "padding-right"), ("pb-", "padding-bottom"), ("pl-", "padding-left")] {
1124 if let Some(value) = class.strip_prefix(prefix) {
1125 return parse_tailwind_spacing(value, &|px| padding_props(&[prop], px));
1126 }
1127 }
1128 if let Some(value) = class.strip_prefix("m-") {
1130 return parse_tailwind_spacing(value, &|px| margin_props(&["margin"], px));
1131 }
1132 if let Some(value) = class.strip_prefix("mx-") {
1133 return parse_tailwind_spacing(value, &|px| margin_props(&["margin-left", "margin-right"], px));
1134 }
1135 if let Some(value) = class.strip_prefix("my-") {
1136 return parse_tailwind_spacing(value, &|px| margin_props(&["margin-top", "margin-bottom"], px));
1137 }
1138 for &(prefix, prop) in &[("mt-", "margin-top"), ("mr-", "margin-right"), ("mb-", "margin-bottom"), ("ml-", "margin-left")] {
1139 if let Some(value) = class.strip_prefix(prefix) {
1140 return parse_tailwind_spacing(value, &|px| margin_props(&[prop], px));
1141 }
1142 }
1143 if let Some(value) = class.strip_prefix("gap-") {
1145 if !value.starts_with("x-") && !value.starts_with("y-") {
1146 return parse_tailwind_spacing(value, &|px| {
1147 let mut props = CssProps::new();
1148 props.insert("gap".into(), json!(format!("{}px", px)));
1149 props
1150 });
1151 }
1152 }
1153 if let Some(value) = class.strip_prefix("gap-x-") {
1154 return parse_tailwind_spacing(value, &|px| {
1155 let mut props = CssProps::new();
1156 props.insert("column-gap".into(), json!(format!("{}px", px)));
1157 props
1158 });
1159 }
1160 if let Some(value) = class.strip_prefix("gap-y-") {
1161 return parse_tailwind_spacing(value, &|px| {
1162 let mut props = CssProps::new();
1163 props.insert("row-gap".into(), json!(format!("{}px", px)));
1164 props
1165 });
1166 }
1167 if let Some(value) = class.strip_prefix("space-x-") {
1169 return parse_tailwind_spacing(value, &|px| {
1170 let mut props = CssProps::new();
1171 props.insert("--space-x".into(), json!(format!("{}px", px)));
1174 props
1175 });
1176 }
1177 if let Some(value) = class.strip_prefix("space-y-") {
1178 return parse_tailwind_spacing(value, &|px| {
1179 let mut props = CssProps::new();
1180 props.insert("--space-y".into(), json!(format!("{}px", px)));
1181 props
1182 });
1183 }
1184 match class {
1186 "font-thin" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("100")); return Some(p); }
1187 "font-extralight" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("200")); return Some(p); }
1188 "font-light" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("300")); return Some(p); }
1189 "font-normal" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("400")); return Some(p); }
1190 "font-medium" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("500")); return Some(p); }
1191 "font-semibold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("600")); return Some(p); }
1192 "font-bold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("700")); return Some(p); }
1193 "font-extrabold" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("800")); return Some(p); }
1194 "font-black" => { let mut p = CssProps::new(); p.insert("font-weight".into(), json!("900")); return Some(p); }
1195 _ => {}
1196 }
1197 match class {
1199 "font-sans" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("system-ui, -apple-system, sans-serif")); return Some(p); }
1200 "font-serif" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("Georgia, serif")); return Some(p); }
1201 "font-mono" => { let mut p = CssProps::new(); p.insert("font-family".into(), json!("ui-monospace, monospace")); return Some(p); }
1202 _ => {}
1203 }
1204 match class {
1206 "text-xs" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("12px")); p.insert("line-height".into(), json!("16px")); return Some(p); }
1207 "text-sm" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("14px")); p.insert("line-height".into(), json!("20px")); return Some(p); }
1208 "text-base" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("16px")); p.insert("line-height".into(), json!("24px")); return Some(p); }
1209 "text-lg" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("18px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
1210 "text-xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("20px")); p.insert("line-height".into(), json!("28px")); return Some(p); }
1211 "text-2xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("24px")); p.insert("line-height".into(), json!("32px")); return Some(p); }
1212 "text-3xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("30px")); p.insert("line-height".into(), json!("36px")); return Some(p); }
1213 "text-4xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("36px")); p.insert("line-height".into(), json!("40px")); return Some(p); }
1214 "text-5xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("48px")); p.insert("line-height".into(), json!("1")); return Some(p); }
1215 "text-6xl" => { let mut p = CssProps::new(); p.insert("font-size".into(), json!("60px")); p.insert("line-height".into(), json!("1")); return Some(p); }
1216 _ => {}
1217 }
1218 match class {
1220 "text-left" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("left")); return Some(p); }
1221 "text-center" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("center")); return Some(p); }
1222 "text-right" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("right")); return Some(p); }
1223 "text-justify" => { let mut p = CssProps::new(); p.insert("text-align".into(), json!("justify")); return Some(p); }
1224 _ => {}
1225 }
1226 match class {
1228 "overflow-auto" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("auto")); return Some(p); }
1229 "overflow-hidden" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("hidden")); return Some(p); }
1230 "overflow-visible" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("visible")); return Some(p); }
1231 "overflow-scroll" => { let mut p = CssProps::new(); p.insert("overflow".into(), json!("scroll")); return Some(p); }
1232 "overflow-x-auto" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("auto")); return Some(p); }
1233 "overflow-x-hidden" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("hidden")); return Some(p); }
1234 "overflow-x-scroll" => { let mut p = CssProps::new(); p.insert("overflow-x".into(), json!("scroll")); return Some(p); }
1235 "overflow-y-auto" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("auto")); return Some(p); }
1236 "overflow-y-hidden" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("hidden")); return Some(p); }
1237 "overflow-y-scroll" => { let mut p = CssProps::new(); p.insert("overflow-y".into(), json!("scroll")); return Some(p); }
1238 _ => {}
1239 }
1240 match class {
1242 "shadow-sm" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 1px 2px 0 rgba(0, 0, 0, 0.05)")); return Some(p); }
1243 "shadow" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)")); return Some(p); }
1244 "shadow-md" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)")); return Some(p); }
1245 "shadow-lg" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)")); return Some(p); }
1246 "shadow-xl" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)")); return Some(p); }
1247 "shadow-2xl" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("0 25px 50px -12px rgba(0, 0, 0, 0.25)")); return Some(p); }
1248 "shadow-none" => { let mut p = CssProps::new(); p.insert("box-shadow".into(), json!("none")); return Some(p); }
1249 _ => {}
1250 }
1251 if let Some(arb_value) = parse_arbitrary_value(class) {
1253 return Some(arb_value);
1254 }
1255 if let Some(rest) = class.strip_prefix("text-") {
1257 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1258 let mut props = CssProps::new();
1259 props.insert("color".into(), json!(hex));
1260 return Some(props);
1261 }
1262 }
1263 if let Some(rest) = class.strip_prefix("bg-") {
1265 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1266 let mut props = CssProps::new();
1267 props.insert("background-color".into(), json!(hex));
1268 return Some(props);
1269 }
1270 }
1271 if let Some(rest) = class.strip_prefix("divide-") {
1273 if let Some(hex) = get_tailwind_color_with_vars(rest, vars) {
1274 let mut props = CssProps::new();
1275 props.insert("border-color".into(), json!(hex));
1276 return Some(props);
1277 }
1278 }
1279 if class == "border" {
1280 return Some(border_props(None, 1, vars));
1281 }
1282 if let Some(rest) = class.strip_prefix("border-") {
1283 let parts: Vec<&str> = rest.split('-').collect();
1291
1292 let valid_sides = ["t", "b", "l", "r", "x", "y"];
1294 let (side, color_or_width_parts) = if parts.len() > 1 && valid_sides.contains(&parts[0]) {
1295 (Some(parts[0]), &parts[1..])
1296 } else {
1297 (None, &parts[..])
1298 };
1299
1300 if color_or_width_parts.len() == 2 {
1302 let color_shade = color_or_width_parts.join("-");
1304 if let Some(hex) = get_tailwind_color_with_vars(&color_shade, vars) {
1305 let mut props = CssProps::new();
1306 let prop_name = if let Some(s) = side {
1307 format!("border-{}-color", s)
1308 } else {
1309 "border-color".to_string()
1310 };
1311 props.insert(prop_name, json!(hex));
1312 return Some(props);
1313 }
1314 }
1315
1316 if color_or_width_parts.len() == 1 {
1318 let potential_color = format!("{}-500", color_or_width_parts[0]);
1319 if let Some(hex) = get_tailwind_color_with_vars(&potential_color, vars) {
1320 let mut props = CssProps::new();
1321 let prop_name = if let Some(s) = side {
1322 format!("border-{}-color", s)
1323 } else {
1324 "border-color".to_string()
1325 };
1326 props.insert(prop_name, json!(hex));
1327 return Some(props);
1328 }
1329 }
1330
1331 if color_or_width_parts.len() == 1 {
1333 if let Ok(width) = color_or_width_parts[0].parse::<i32>() {
1334 return Some(border_props(side, width, vars));
1335 }
1336 }
1337 }
1338 if class == "rounded" { return Some(rounded_props(None, Some("md"))); }
1340 if let Some(sz) = class.strip_prefix("rounded-") {
1341 return Some(rounded_props(None, Some(sz)));
1342 }
1343 for &(pref, side) in &[("rounded-t", "t"), ("rounded-b", "b"), ("rounded-l", "l"), ("rounded-r", "r")] {
1344 if class == pref { return Some(rounded_props(Some(side), Some("md"))); }
1345 if let Some(sz) = class.strip_prefix(&(pref.to_string() + "-")) {
1346 return Some(rounded_props(Some(side), Some(sz)));
1347 }
1348 }
1349 if let Some(cur) = class.strip_prefix("cursor-") {
1351 let mut props = CssProps::new();
1352 props.insert("cursor".into(), json!(match cur {
1353 "pointer" => "pointer",
1354 "default" => "default",
1355 "text" => "text",
1356 "move" => "move",
1357 "wait" => "wait",
1358 "not-allowed" => "not-allowed",
1359 other => other,
1360 }));
1361 return Some(props);
1362 }
1363 if class == "transition" || class == "transition-all" {
1365 let mut props = CssProps::new();
1366 props.insert("transition-property".into(), json!("all"));
1367 props.insert("transition-duration".into(), json!("150ms"));
1368 props.insert("transition-timing-function".into(), json!("ease-in-out"));
1369 return Some(props);
1370 }
1371 if class == "transition-none" {
1372 let mut props = CssProps::new();
1373 props.insert("transition-property".into(), json!("none"));
1374 props.insert("transition-duration".into(), json!("0ms"));
1375 return Some(props);
1376 }
1377 if let Some(rest) = class.strip_prefix("transition-") {
1378 let mut props = CssProps::new();
1380 let property = match rest {
1381 "colors" => "color, background-color, border-color, fill, stroke",
1382 "opacity" => "opacity",
1383 "transform" => "transform",
1384 "shadow" => "box-shadow",
1385 other => other,
1386 };
1387 props.insert("transition-property".into(), json!(property));
1388 props.insert("transition-duration".into(), json!("150ms"));
1389 props.insert("transition-timing-function".into(), json!("ease-in-out"));
1390 return Some(props);
1391 }
1392 if let Some(val) = class.strip_prefix("w-") {
1394 return width_like_props("width", val);
1395 }
1396 if let Some(val) = class.strip_prefix("min-w-") {
1397 return width_like_props("min-width", val);
1398 }
1399 if let Some(val) = class.strip_prefix("max-w-") {
1400 return width_like_props("max-width", val);
1401 }
1402 if let Some(val) = class.strip_prefix("h-") {
1404 return width_like_props("height", val);
1405 }
1406 if let Some(val) = class.strip_prefix("min-h-") {
1407 return width_like_props("min-height", val);
1408 }
1409 if let Some(val) = class.strip_prefix("max-h-") {
1410 return width_like_props("max-height", val);
1411 }
1412 None
1413}
1414
1415fn parse_tailwind_spacing<F>(value: &str, builder: &F) -> Option<CssProps>
1416where
1417 F: Fn(i32) -> CssProps,
1418{
1419 if let Ok(n) = value.parse::<i32>() {
1420 let px = n * 4;
1421 return Some(builder(px));
1422 }
1423 None
1424}
1425
1426fn padding_props(keys: &[&str], px_value: i32) -> CssProps {
1427 let mut props = CssProps::new();
1428 let val = format!("{}px", px_value);
1429 for key in keys {
1430 props.insert((*key).into(), json!(&val));
1431 }
1432 props
1433}
1434
1435fn margin_props(keys: &[&str], px_value: i32) -> CssProps {
1436 let mut props = CssProps::new();
1437 let val = format!("{}px", px_value);
1438 for key in keys {
1439 props.insert((*key).into(), json!(&val));
1440 }
1441 props
1442}
1443
1444fn border_props(side: Option<&str>, width: i32, _vars: &IndexMap<String, String>) -> CssProps {
1445 let mut props = CssProps::new();
1446 let width_str = format!("{}px", width);
1447 match side {
1448 None => {
1449 props.insert("border-width".into(), json!(&width_str));
1450 }
1451 Some("t") => {
1452 props.insert("border-top-width".into(), json!(&width_str));
1453 }
1454 Some("b") => {
1455 props.insert("border-bottom-width".into(), json!(&width_str));
1456 }
1457 Some("l") => {
1458 props.insert("border-left-width".into(), json!(&width_str));
1459 }
1460 Some("r") => {
1461 props.insert("border-right-width".into(), json!(&width_str));
1462 }
1463 Some("x") => {
1464 props.insert("border-left-width".into(), json!(&width_str));
1465 props.insert("border-right-width".into(), json!(&width_str));
1466 }
1467 Some("y") => {
1468 props.insert("border-top-width".into(), json!(&width_str));
1469 props.insert("border-bottom-width".into(), json!(&width_str));
1470 }
1471 _ => {
1472 props.insert("border-width".into(), json!(&width_str));
1473 }
1474 };
1475 props.insert("border-color".into(), json!("var(border)"));
1476 props.insert("border-style".into(), json!("solid"));
1477 props
1478}
1479
1480fn rounded_props(side: Option<&str>, size: Option<&str>) -> CssProps {
1481 let mut props = CssProps::new();
1482 let px = match size.unwrap_or("md") {
1483 "none" => 0,
1484 "sm" => 2,
1485 "md" => 4,
1486 "lg" => 8,
1487 "xl" => 12,
1488 "2xl" => 16,
1489 "3xl" => 24,
1490 "full" => 9999,
1491 s => s.parse::<i32>().unwrap_or(4),
1492 };
1493 let v = json!(format!("{}px", px));
1494 match side {
1495 None => { props.insert("border-radius".into(), v); }
1496 Some("t") => {
1497 props.insert("border-top-left-radius".into(), v.clone());
1498 props.insert("border-top-right-radius".into(), v);
1499 }
1500 Some("b") => {
1501 props.insert("border-bottom-left-radius".into(), v.clone());
1502 props.insert("border-bottom-right-radius".into(), v);
1503 }
1504 Some("l") => { props.insert("border-top-left-radius".into(), v.clone()); props.insert("border-bottom-left-radius".into(), v); }
1505 Some("r") => { props.insert("border-top-right-radius".into(), v.clone()); props.insert("border-bottom-right-radius".into(), v); }
1506 _ => { props.insert("border-radius".into(), v); }
1507 }
1508 props
1509}
1510
1511fn width_like_props(prop: &str, token: &str) -> Option<CssProps> {
1512 let mut props = CssProps::new();
1513 let value = match token {
1514 "full" => Some("100%".to_string()),
1515 "screen" => Some(if prop == "width" { "100vw" } else { "100vh" }.to_string()),
1516 "min" => Some("min-content".to_string()),
1517 "max" => Some("max-content".to_string()),
1518 "fit" => Some("fit-content".to_string()),
1519 "auto" => Some("auto".to_string()),
1520 "px" => Some("1px".to_string()),
1521 other => {
1522 if let Some((a, b)) = other.split_once('/') {
1524 if let (Ok(na), Ok(nb)) = (a.parse::<f64>(), b.parse::<f64>()) {
1525 let pct = (na / nb) * 100.0;
1526 Some(format!("{}%", trim_trailing_zeros(pct)))
1527 } else { None }
1528 } else if let Ok(n) = other.parse::<i32>() {
1529 Some(format!("{}px", n * 4))
1530 } else {
1531 None
1532 }
1533 }
1534 }?;
1535 props.insert(prop.into(), json!(value));
1536 Some(props)
1537}
1538
1539fn trim_trailing_zeros(num: f64) -> String {
1540 let mut s = format!("{:.6}", num);
1541 while s.contains('.') && s.ends_with('0') { s.pop(); }
1542 if s.ends_with('.') { s.pop(); }
1543 s
1544}
1545
1546fn css_escape_class(class: &str) -> String { class.replace(':', "\\:") }
1551
1552fn class_to_selector(class: &str) -> String {
1553 let (_bp, hover, base) = parse_prefixed_class(class);
1554 if hover {
1555 format!(".{}:hover", css_escape_class(&base))
1556 } else {
1557 format!(".{}", css_escape_class(&base))
1558 }
1559}
1560
1561pub fn post_process_css(
1568 raw_rules: &[(String, CssProps)],
1569 vars: &IndexMap<String, String>,
1570) -> String {
1571 let mut normal = vec![];
1573 let mut media_map: IndexMap<String, Vec<(String, CssProps)>> = IndexMap::new();
1574 for (sel, props) in raw_rules.iter() {
1575 if let Some((media, inner)) = sel.split_once('{') {
1576 if media.trim_start().starts_with("@media ") && inner.ends_with('}') {
1577 let inner_sel = inner.trim_end_matches('}').to_string();
1578 media_map
1579 .entry(media.trim().to_string())
1580 .or_default()
1581 .push((inner_sel, props.clone()));
1582 continue;
1583 }
1584 }
1585 normal.push((sel.clone(), props.clone()));
1586 }
1587 let mut out = String::new();
1588 for (sel, props) in normal {
1589 out.push_str(&sel);
1590 out.push('{');
1591 out.push_str(&css_props_string(&props, vars));
1592 out.push_str("}\n");
1593 }
1594 for (media, entries) in media_map {
1595 out.push_str(&media);
1596 out.push('{');
1597 for (sel, props) in entries {
1598 out.push_str(&sel);
1599 out.push('{');
1600 out.push_str(&css_props_string(&props, vars));
1601 out.push_str("}");
1602 }
1603 out.push_str("}\n");
1604 }
1605 out
1606}
1607
1608fn parse_prefixed_class(class: &str) -> (Option<String>, bool, String) {
1611 let parts: Vec<&str> = class.split(':').collect();
1613 if parts.len() == 1 {
1614 return (None, false, class.to_string());
1615 }
1616 let mut bp: Option<String> = None;
1617 let mut hover = false;
1618 for &p in &parts[..parts.len() - 1] {
1619 match p {
1620 "hover" => hover = true,
1621 "xs" | "sm" | "md" | "lg" | "xl" => bp = Some(p.to_string()),
1622 _ => {}
1623 }
1624 }
1625 let base = parts.last().unwrap().to_string();
1626 (bp, hover, base)
1627}
1628
1629fn wrap_with_media(selector: &str, bp_key: Option<&str>, bps: &IndexMap<String, String>) -> String {
1630 if let Some(k) = bp_key {
1631 if let Some(val) = bps.get(k) {
1632 return format!("@media (min-width: {}) {{{}}}", val, selector);
1633 }
1634 }
1635 selector.to_string()
1636}
1637
1638fn get_tailwind_color(color_shade: &str) -> Option<String> {
1640 let parts: Vec<&str> = color_shade.split('-').collect();
1641 if parts.len() != 2 {
1642 return None;
1643 }
1644 let color_name = parts[0];
1645 let shade = parts[1];
1646
1647 if let Some(hex) = TAILWIND_COLORS
1649 .get(color_name)
1650 .and_then(|shades| shades.get(shade))
1651 {
1652 return Some(hex.to_string());
1653 }
1654
1655 None
1656}
1657
1658fn get_tailwind_color_with_vars(color_shade: &str, vars: &IndexMap<String, String>) -> Option<String> {
1659 if let Some(hex) = get_tailwind_color(color_shade) {
1661 return Some(hex);
1662 }
1663
1664 if let Some(val) = vars.get(color_shade) {
1673 return Some(val.clone());
1674 }
1675
1676 if let Some(val) = vars.get(&format!("colors.{}", color_shade)) {
1678 return Some(val.clone());
1679 }
1680
1681 if let Some(val) = vars.get(&format!("color.{}", color_shade)) {
1683 return Some(val.clone());
1684 }
1685
1686 let parts: Vec<&str> = color_shade.split('-').collect();
1689 if parts.len() >= 1 {
1690 let color_name = parts[0];
1691
1692 if let Some(val) = vars.get(color_name) {
1694 return Some(val.clone());
1695 }
1696
1697 if let Some(val) = vars.get(&format!("color.{}", color_name)) {
1699 return Some(val.clone());
1700 }
1701 }
1702
1703 None
1704}
1705
1706fn parse_arbitrary_value(class: &str) -> Option<CssProps> {
1708 if let Some(bracket_start) = class.find('[') {
1710 if !class.ends_with(']') {
1711 return None;
1712 }
1713 let prefix = &class[..bracket_start];
1714 let value = &class[bracket_start + 1..class.len() - 1];
1715
1716 let mut props = CssProps::new();
1717 match prefix {
1718 "bg" => {
1719 props.insert("background-color".into(), json!(value));
1720 return Some(props);
1721 }
1722 "text" => {
1723 props.insert("color".into(), json!(value));
1724 return Some(props);
1725 }
1726 "border" => {
1727 props.insert("border-color".into(), json!(value));
1728 return Some(props);
1729 }
1730 "divide" => {
1731 props.insert("border-color".into(), json!(value));
1732 return Some(props);
1733 }
1734 _ => return None,
1735 }
1736 }
1737 None
1738}
1739
1740pub mod api {
1742 pub use super::{SelectorStyles, State};
1743}
1744
1745#[cfg(test)]
1746mod tests {
1747 use super::*;
1748
1749 #[test]
1750 fn default_theme_has_p2() {
1751 let mut st = State::new_default();
1752 st.register_tailwind_classes(["p-2".to_string()]);
1753 let css = st.css_for_web();
1754 assert!(css.contains(".p-2{"));
1755 assert!(css.contains("padding:8px"));
1756 }
1757
1758 #[test]
1759 fn rn_conversion() {
1760 let mut st = State::new_default();
1761 let mut styles = IndexMap::new();
1763 let mut button_props = IndexMap::new();
1764 button_props.insert("backgroundColor".to_string(), json!("#007bff"));
1765 styles.insert("button".to_string(), button_props);
1766 st.add_theme("default", styles);
1767 st.set_theme("default").ok();
1768
1769 let out = st.rn_styles_for("button", &[]);
1770 assert!(out.get("backgroundColor").is_some());
1771 }
1772
1773 #[test]
1774 fn embedded_defaults_and_version() {
1775 let mut st = State::default_state();
1777 st.add_theme("default", IndexMap::new());
1778 st.set_theme("default").ok();
1779
1780 let mut vars = IndexMap::new();
1781 vars.insert("primary".to_string(), "#007bff".to_string());
1782 st.set_variables(vars);
1783
1784 assert!(st.themes.contains_key("default"));
1785 let def = st.themes.get("default").unwrap();
1786 assert!(def.variables.contains_key("primary"));
1787
1788 #[cfg(target_arch = "wasm32")]
1791 {
1792 let v = get_version();
1793 assert!(!v.is_empty());
1794 }
1795 }
1796
1797 #[test]
1798 fn border_color_with_direction() {
1799 let mut st = State::new_default();
1800
1801 st.register_tailwind_classes(["border-b-blue-500".to_string()]);
1803 let css = st.css_for_web();
1804 assert!(css.contains(".border-b-blue-500{"));
1805 assert!(css.contains("border-bottom-color:#3b82f6") || css.contains("border-b-color:#3b82f6"));
1806
1807 st.register_tailwind_classes(["border-t-red-500".to_string()]);
1809 let css = st.css_for_web();
1810 assert!(css.contains(".border-t-red-500{"));
1811
1812 st.register_tailwind_classes(["border-blue-500".to_string()]);
1814 let css = st.css_for_web();
1815 assert!(css.contains(".border-blue-500{"));
1816 assert!(css.contains("border-color:#3b82f6"));
1817 }
1818
1819 #[test]
1820 fn border_width_with_direction() {
1821 let mut st = State::new_default();
1822
1823 st.register_tailwind_classes(["border-b-2".to_string()]);
1825 let css = st.css_for_web();
1826 assert!(css.contains(".border-b-2{"));
1827 assert!(css.contains("border-bottom-width:2px"));
1828
1829 st.register_tailwind_classes(["border-2".to_string()]);
1831 let css = st.css_for_web();
1832 assert!(css.contains(".border-2{"));
1833 assert!(css.contains("border-width:2px"));
1834 }
1835
1836 #[test]
1837 fn display_flex_hover_breakpoint() {
1838 let mut st = State::new_default();
1839
1840 st.add_theme("default", IndexMap::new());
1842 st.set_theme("default").ok();
1843
1844 let mut breakpoints = IndexMap::new();
1845 breakpoints.insert("md".to_string(), "768px".to_string());
1846 st.set_breakpoints(breakpoints);
1847
1848 st.register_tailwind_classes([
1849 "block".into(),
1850 "inline-flex".into(),
1851 "hidden".into(),
1852 "md:flex".into(),
1853 "md:hover:block".into(),
1854 ]);
1855 let css = st.css_for_web();
1856 assert!(css.contains(".block{"));
1857 assert!(css.contains("display:block"));
1858 assert!(css.contains(".inline-flex{"));
1859 assert!(css.contains("display:inline-flex"));
1860 assert!(css.contains(".hidden{"));
1861 assert!(css.contains("display:none"));
1862 assert!(css.contains("@media (min-width: 768px)"));
1864 assert!(css.contains(".flex{display:flex"));
1865 assert!(css.contains(":hover{display:block"));
1867
1868 let rn = st.rn_styles_for("div", &["md:flex".into()]);
1870 assert_eq!(rn.get("display").and_then(|v| v.as_str()), Some("flex"));
1871 }
1872
1873 #[test]
1874 fn parse_var_references_basic() {
1875 let refs = parse_var_references("var(color)");
1877 assert_eq!(refs.len(), 1);
1878 assert_eq!(refs[0].2, "color");
1879 assert_eq!(refs[0].0, 0); assert_eq!(refs[0].1, 10); let refs = parse_var_references("var(--primary)");
1884 assert_eq!(refs.len(), 1);
1885 assert_eq!(refs[0].2, "primary");
1886
1887 let refs = parse_var_references("var(--color) and var(size)");
1889 assert_eq!(refs.len(), 2);
1890 assert_eq!(refs[0].2, "color");
1891 assert_eq!(refs[1].2, "size");
1892
1893 let refs = parse_var_references("var( --spacing )");
1895 assert_eq!(refs.len(), 1);
1896 assert_eq!(refs[0].2, "spacing");
1897
1898 let refs = parse_var_references("var(color.primary-500)");
1900 assert_eq!(refs.len(), 1);
1901 assert_eq!(refs[0].2, "color.primary-500");
1902
1903 let refs = parse_var_references("no variables here");
1905 assert_eq!(refs.len(), 0);
1906
1907 let refs = parse_var_references("var(");
1909 assert_eq!(refs.len(), 0);
1910
1911 let refs = parse_var_references("var(color");
1913 assert_eq!(refs.len(), 0);
1914 }
1915
1916 #[test]
1917 fn resolve_vars_basic() {
1918 let mut vars = IndexMap::new();
1919 vars.insert("primary".to_string(), "#ff0000".to_string());
1920 vars.insert("spacing".to_string(), "8px".to_string());
1921 vars.insert("color.blue".to_string(), "#0000ff".to_string());
1922
1923 assert_eq!(resolve_vars("var(--primary)", &vars), "#ff0000");
1925 assert_eq!(resolve_vars("var(primary)", &vars), "#ff0000");
1926 assert_eq!(resolve_vars("var( --primary )", &vars), "#ff0000");
1927
1928 assert_eq!(
1930 resolve_vars("var(--primary) var(--spacing)", &vars),
1931 "#ff0000 8px"
1932 );
1933
1934 assert_eq!(resolve_vars("var(--color.blue)", &vars), "#0000ff");
1936
1937 assert_eq!(resolve_vars("var(--undefined)", &vars), "var(--undefined)");
1939
1940 assert_eq!(resolve_vars("$primary", &vars), "#ff0000");
1942
1943 assert_eq!(resolve_vars("plain text", &vars), "plain text");
1945 }
1946
1947 #[test]
1948 fn resolve_vars_edge_cases() {
1949 let mut vars = IndexMap::new();
1950 vars.insert("a".to_string(), "1".to_string());
1951 vars.insert("b".to_string(), "2".to_string());
1952
1953 assert_eq!(resolve_vars("var(a)var(b)", &vars), "12");
1955
1956 assert_eq!(resolve_vars("prefix var(a) suffix", &vars), "prefix 1 suffix");
1958
1959 assert_eq!(resolve_vars("", &vars), "");
1961
1962 vars.insert("var123".to_string(), "value".to_string());
1964 assert_eq!(resolve_vars("var(var123)", &vars), "value");
1965
1966 vars.insert("my_var".to_string(), "test".to_string());
1968 assert_eq!(resolve_vars("var(my_var)", &vars), "test");
1969 }
1970}
1971
1972#[cfg(all(target_os = "android", feature = "android"))]
1973#[cfg(feature = "android")]
1974mod android_jni;
1975
1976mod bridge_common;
1977mod ffi;
1978
1979pub use ffi::*;
1980
1981#[cfg(target_vendor = "apple")]
1982mod ios_ffi;