Skip to main content

stylex_transform/transform/styleq/
common.rs

1use std::collections::BTreeMap;
2use std::rc::Rc;
3
4use indexmap::IndexMap;
5use log::error;
6use std::collections::hash_map::DefaultHasher;
7use std::hash::{Hash, Hasher};
8use stylex_macros::{stylex_panic, stylex_unimplemented, stylex_unreachable};
9
10use crate::shared::enums::data_structures::flat_compiled_styles_value::FlatCompiledStylesValue;
11use crate::shared::structures::types::FlatCompiledStyles;
12use crate::shared::utils::core::parse_nullable_style::{ResolvedArg, StyleObject};
13use stylex_constants::constants::common::COMPILED_KEY;
14
15pub(crate) struct StyleQResult {
16  pub(crate) class_name: String,
17  pub(crate) inline_style: Option<FlatCompiledStyles>,
18  pub(crate) data_style_src: Option<String>,
19}
20
21fn get_hash<T>(obj: T) -> u64
22where
23  T: Hash,
24{
25  let mut hasher = DefaultHasher::new();
26  obj.hash(&mut hasher);
27  hasher.finish()
28}
29
30pub(crate) fn styleq(arguments: &[ResolvedArg]) -> StyleQResult {
31  let mut class_name = String::default();
32  let mut debug_string = String::default();
33
34  if arguments.is_empty() {
35    return StyleQResult {
36      class_name,
37      inline_style: None,
38      data_style_src: None,
39    };
40  }
41
42  let mut defined_properties: Vec<String> = vec![];
43  let mut inline_style: Option<FlatCompiledStyles> = None;
44  let mut next_cache: Option<IndexMap<u64, (String, Vec<String>, String)>> = Some(IndexMap::new());
45  let mut styles = arguments.iter().collect::<Vec<_>>();
46
47  // Iterate over styles from last to first (pop from end)
48  while let Some(possible_style) = styles.pop() {
49    let possible_style = match possible_style {
50      ResolvedArg::StyleObject(_, _, _) => possible_style,
51      ResolvedArg::ConditionalStyle(_, value, _, _, _) => {
52        if value.is_some() {
53          possible_style
54        } else {
55          continue;
56        }
57      },
58    };
59
60    match possible_style {
61      ResolvedArg::StyleObject(style, _, _) => match style {
62        StyleObject::Style(style) => {
63          if let Some(compiled_key) = style.get(COMPILED_KEY) {
64            // ----- COMPILED: Process compiled style object (has $$css) -----
65            if let FlatCompiledStylesValue::Bool(_) | FlatCompiledStylesValue::String(_) =
66              compiled_key.as_ref()
67            {
68              let mut class_name_chunk = String::default();
69
70              // Try cache read first
71              let cache_hit = if let Some(ref next_cache) = next_cache {
72                let btree_map: BTreeMap<_, _> = style.iter().collect();
73                let style_hash = get_hash(btree_map);
74
75                next_cache.get(&style_hash).map(|entry| {
76                  (
77                    style_hash,
78                    entry.0.clone(),
79                    entry.1.clone(),
80                    entry.2.clone(),
81                  )
82                })
83              } else {
84                None
85              };
86
87              if let Some((_hash, cached_class_name, cached_properties, cached_debug_string)) =
88                cache_hit
89              {
90                // Cache hit
91                class_name_chunk = cached_class_name;
92                defined_properties.extend(cached_properties);
93                debug_string = cached_debug_string;
94              } else {
95                // Compute from scratch
96                let mut defined_properties_chunk: Vec<String> = vec![];
97
98                for (prop, value) in style.iter() {
99                  if prop.eq(COMPILED_KEY) {
100                    let compiled_key_value = &style[prop];
101
102                    let mut compiled_key_value_is_true = false;
103
104                    if let FlatCompiledStylesValue::Bool(value) = compiled_key_value.as_ref()
105                      && *value
106                    {
107                      compiled_key_value_is_true = true;
108                    }
109
110                    if !compiled_key_value_is_true {
111                      let compiled_key_string_value = match compiled_key_value.as_ref() {
112                        FlatCompiledStylesValue::String(strng) => strng.clone(),
113                        other => {
114                          let other_debug_info = format!("{:?}", other);
115                          let variant_name =
116                            other_debug_info.split("::").last().unwrap_or("unknown");
117
118                          stylex_unimplemented!(
119                            "String conversion not implemented for FlatCompiledStylesValue::{}",
120                            variant_name
121                          )
122                        },
123                      };
124
125                      debug_string = if !debug_string.is_empty() {
126                        format!("{}; {}", compiled_key_string_value, debug_string)
127                      } else {
128                        compiled_key_string_value
129                      };
130                    }
131
132                    continue;
133                  }
134
135                  // Each property value should be a string or null
136                  match value.as_ref() {
137                    FlatCompiledStylesValue::String(_) | FlatCompiledStylesValue::Null => {
138                      if !defined_properties.contains(prop) {
139                        defined_properties.push(prop.clone());
140                        if next_cache.is_some() {
141                          defined_properties_chunk.push(prop.clone());
142                        }
143
144                        if let FlatCompiledStylesValue::String(value) = value.as_ref() {
145                          class_name_chunk = if class_name_chunk.is_empty() {
146                            value.to_string()
147                          } else {
148                            format!("{} {}", class_name_chunk, value)
149                          };
150                        }
151                        // Null: property is defined (won't be overridden) but
152                        // no class name is added — matches JS `null` handling.
153                      }
154                    },
155                    _ => {
156                      error!(
157                        "styleq: {} typeof {:?} is not \"string\" or \"null\".",
158                        prop, value
159                      );
160                    },
161                  }
162                }
163
164                // Cache write (only when cache is active)
165                if let Some(ref mut cache) = next_cache {
166                  let btree_map: BTreeMap<_, _> = style.iter().collect();
167                  let style_hash = get_hash(btree_map);
168                  cache.insert(
169                    style_hash,
170                    (
171                      class_name_chunk.clone(),
172                      defined_properties_chunk,
173                      debug_string.clone(),
174                    ),
175                  );
176                }
177              }
178
179              if !class_name_chunk.is_empty() {
180                class_name = if class_name.is_empty() {
181                  class_name_chunk
182                } else if !class_name.contains(class_name_chunk.as_str()) {
183                  format!("{} {}", class_name_chunk, class_name)
184                } else {
185                  class_name
186                };
187              }
188            } else {
189              stylex_panic!(
190                "styleq: {:#?} typeof {:?} is not \"string\" or \"null\".",
191                compiled_key,
192                "Bool"
193              )
194            }
195          } else {
196            // ----- DYNAMIC: Process inline style object (no $$css) -----
197            let mut sub_style: Option<FlatCompiledStyles> = None;
198
199            for (prop, value) in style.iter() {
200              if !defined_properties.contains(prop) {
201                match value.as_ref() {
202                  FlatCompiledStylesValue::Null => {
203                    // null values mark the property as defined but don't
204                    // contribute to the inline style output.
205                    defined_properties.push(prop.clone());
206                  },
207                  _ => {
208                    if sub_style.is_none() {
209                      sub_style = Some(IndexMap::new());
210                    }
211                    if let Some(ref mut sub) = sub_style {
212                      sub.insert(prop.clone(), Rc::new(value.as_ref().clone()));
213                    }
214                    defined_properties.push(prop.clone());
215                  },
216                }
217              }
218            }
219
220            if let Some(sub) = sub_style {
221              // Merge: sub_style first, then existing inline_style (earlier
222              // properties win, matching JS `Object.assign(subStyle, inlineStyle)`)
223              inline_style = if let Some(existing) = inline_style {
224                let mut merged = sub;
225                for (k, v) in existing {
226                  merged.entry(k).or_insert(v);
227                }
228                Some(merged)
229              } else {
230                Some(sub)
231              };
232            }
233
234            // Cache is unnecessary overhead when inline styles are present
235            next_cache = None;
236          }
237        },
238        StyleObject::Nullable => {},
239        StyleObject::Other => {
240          stylex_panic!("Only compiled StyleX style objects are allowed in styleq().")
241        },
242        StyleObject::Unreachable => {
243          stylex_unreachable!(
244            "Encountered an unexpected style object variant in styleq processing."
245          )
246        },
247      },
248      _ => stylex_unreachable!("Unexpected ResolvedArg variant in styleq loop"),
249    };
250  }
251
252  StyleQResult {
253    class_name,
254    inline_style,
255    data_style_src: Some(debug_string),
256  }
257}