Skip to main content

stylex_css/utils/
when.rs

1use log::debug;
2
3use stylex_structures::stylex_state_options::StyleXStateOptions;
4
5pub fn from_proxy(_value: &StyleXStateOptions) -> Option<String> {
6  debug!("from_proxy is not implemented");
7  None
8}
9
10pub fn from_stylex_style(_value: &StyleXStateOptions) -> Option<String> {
11  debug!("from_stylex_style is not implemented");
12  None
13}
14
15/// Gets the default marker class name based on options
16fn get_default_marker_class_name(options: &StyleXStateOptions) -> String {
17  if let Some(value_from_proxy) = from_proxy(options) {
18    return value_from_proxy;
19  }
20
21  if let Some(value_from_style_xstyle) = from_stylex_style(options) {
22    return value_from_style_xstyle;
23  }
24
25  let prefix = if !options.class_name_prefix.is_empty() {
26    format!("{}-", options.class_name_prefix)
27  } else {
28    String::new()
29  };
30  format!("{}default-marker", prefix)
31}
32
33/// Validates that a pseudo selector starts with ':' but not '::'
34fn validate_pseudo_selector(pseudo: &str) -> Result<(), String> {
35  if !pseudo.starts_with(':') && !pseudo.starts_with('[') {
36    return Err("Pseudo selector must start with \":\" or \"[\"".to_string());
37  }
38
39  if pseudo.starts_with("::") {
40    return Err(
41      "Pseudo selector cannot start with \"::\" (pseudo-elements are not supported)".to_string(),
42    );
43  }
44
45  if pseudo.starts_with("[") {
46    if !pseudo.ends_with("]") {
47      return Err("Attribute selector must end with \"]\"".to_string());
48    }
49
50    // Validate proper bracket matching and quote pairing
51    if !is_valid_attribute_selector(pseudo) {
52      return Err(
53        "Attribute selector has invalid format (mismatched brackets or quotes)".to_string(),
54      );
55    }
56  }
57
58  Ok(())
59}
60
61/// Validates that an attribute selector has proper bracket and quote matching
62fn is_valid_attribute_selector(selector: &str) -> bool {
63  let chars: Vec<char> = selector.chars().collect();
64  if chars.is_empty() || chars[0] != '[' || chars[chars.len() - 1] != ']' {
65    return false;
66  }
67
68  let mut in_single_quote = false;
69  let mut in_double_quote = false;
70  let mut bracket_count = 0;
71
72  for i in 0..chars.len() {
73    let c = chars[i];
74
75    // Track quote state
76    if c == '\'' && (i == 0 || chars[i - 1] != '\\') {
77      in_single_quote = !in_single_quote;
78    } else if c == '"' && (i == 0 || chars[i - 1] != '\\') {
79      in_double_quote = !in_double_quote;
80    }
81
82    // Track brackets only outside quotes
83    if !in_single_quote && !in_double_quote {
84      if c == '[' {
85        bracket_count += 1;
86        // CSS attribute selectors can only have one opening bracket (at the start)
87        if bracket_count > 1 {
88          return false;
89        }
90      } else if c == ']' {
91        bracket_count -= 1;
92        // Closing bracket should only reach 0 at the end
93        if bracket_count < 0 || (bracket_count == 0 && i < chars.len() - 1) {
94          return false;
95        }
96      }
97    }
98  }
99
100  // Should end with exactly one bracket pair and no unclosed quotes
101  bracket_count == 0 && !in_single_quote && !in_double_quote
102}
103
104/// Creates selector that observes if the given pseudo-class is
105/// active on an ancestor with the "defaultMarker"
106///
107/// # Arguments
108/// * `pseudo` - The pseudo selector (e.g., ':hover', ':focus')
109/// * `options` - Either a custom marker string or StyleXStateOptions reference
110///
111/// # Returns
112/// A :where() clause for the ancestor observer
113pub fn ancestor(pseudo: &str, options: Option<&StyleXStateOptions>) -> Result<String, String> {
114  validate_pseudo_selector(pseudo)?;
115  let default_marker = options
116    .map(get_default_marker_class_name)
117    .unwrap_or_else(|| "x-default-marker".to_string());
118  Ok(format!(":where(.{}{} *)", default_marker, pseudo))
119}
120
121/// Creates selector that observes if the given pseudo-class is
122/// active on a descendant with the "defaultMarker"
123///
124/// # Arguments
125/// * `pseudo` - The pseudo selector (e.g., ':hover', ':focus')
126/// * `options` - Either a custom marker string or StyleXStateOptions reference
127///
128/// # Returns
129/// A :has() clause for the descendant observer
130pub fn descendant(pseudo: &str, options: Option<&StyleXStateOptions>) -> Result<String, String> {
131  validate_pseudo_selector(pseudo)?;
132  let default_marker = options
133    .map(get_default_marker_class_name)
134    .unwrap_or_else(|| "x-default-marker".to_string());
135  Ok(format!(":where(:has(.{}{}))", default_marker, pseudo))
136}
137
138/// Creates selector that observes if the given pseudo-class is
139/// active on a previous sibling with the "defaultMarker"
140///
141/// # Arguments
142/// * `pseudo` - The pseudo selector (e.g., ':hover', ':focus')
143/// * `options` - Either a custom marker string or StyleXStateOptions reference
144///
145/// # Returns
146/// A :where() clause for the previous sibling observer
147pub fn sibling_before(
148  pseudo: &str,
149  options: Option<&StyleXStateOptions>,
150) -> Result<String, String> {
151  validate_pseudo_selector(pseudo)?;
152  let default_marker = options
153    .map(get_default_marker_class_name)
154    .unwrap_or_else(|| "x-default-marker".to_string());
155  Ok(format!(":where(.{}{} ~ *)", default_marker, pseudo))
156}
157
158/// Creates selector that observes if the given pseudo-class is
159/// active on a next sibling with the "defaultMarker"
160///
161/// # Arguments
162/// * `pseudo` - The pseudo selector (e.g., ':hover', ':focus')
163/// * `options` - Either a custom marker string or StyleXStateOptions reference
164///
165/// # Returns
166/// A :has() clause for the next sibling observer
167pub fn sibling_after(pseudo: &str, options: Option<&StyleXStateOptions>) -> Result<String, String> {
168  validate_pseudo_selector(pseudo)?;
169  let default_marker = options
170    .map(get_default_marker_class_name)
171    .unwrap_or_else(|| "x-default-marker".to_string());
172  Ok(format!(":where(:has(~ .{}{}))", default_marker, pseudo))
173}
174
175/// Creates selector that observes if the given pseudo-class is
176/// active on any sibling with the "defaultMarker"
177///
178/// # Arguments
179/// * `pseudo` - The pseudo selector (e.g., ':hover', ':focus')
180/// * `options` - Either a custom marker string or StyleXStateOptions reference
181///
182/// # Returns
183/// A :where() clause for the any sibling observer
184pub fn any_sibling(pseudo: &str, options: Option<&StyleXStateOptions>) -> Result<String, String> {
185  validate_pseudo_selector(pseudo)?;
186  let default_marker = options
187    .map(get_default_marker_class_name)
188    .unwrap_or_else(|| "x-default-marker".to_string());
189  Ok(format!(
190    ":where(.{}{} ~ *, :has(~ .{}{}))",
191    default_marker, pseudo, default_marker, pseudo
192  ))
193}
194
195#[cfg(test)]
196mod tests {
197  use super::*;
198
199  #[test]
200  fn test_validate_pseudo_selector_valid() {
201    assert!(validate_pseudo_selector(":hover").is_ok());
202    assert!(validate_pseudo_selector(":focus").is_ok());
203    assert!(validate_pseudo_selector(":active").is_ok());
204  }
205
206  #[test]
207  fn test_validate_pseudo_selector_invalid_no_colon() {
208    let result = validate_pseudo_selector("hover");
209    assert!(result.is_err());
210    assert_eq!(
211      result.unwrap_err(),
212      "Pseudo selector must start with \":\" or \"[\""
213    );
214  }
215
216  #[test]
217  fn test_validate_pseudo_selector_invalid_double_colon() {
218    let result = validate_pseudo_selector("::before");
219    assert!(result.is_err());
220    assert!(result.unwrap_err().contains("pseudo-elements"));
221  }
222
223  #[test]
224  fn test_validate_pseudo_selector_valid_attribute() {
225    assert!(validate_pseudo_selector("[data-state=\"open\"]").is_ok());
226    assert!(validate_pseudo_selector("[data-state='open']").is_ok());
227    assert!(validate_pseudo_selector("[disabled]").is_ok());
228    assert!(validate_pseudo_selector("[aria-label*=\"test\"]").is_ok());
229  }
230
231  #[test]
232  fn test_validate_pseudo_selector_invalid_attribute_missing_bracket() {
233    let result = validate_pseudo_selector("[data-state=\"open\"");
234    assert!(result.is_err());
235    assert!(result.unwrap_err().contains("must end with"));
236  }
237
238  #[test]
239  fn test_validate_pseudo_selector_invalid_attribute_mismatched_quotes() {
240    let result = validate_pseudo_selector("[data-state=\"open']");
241    assert!(result.is_err());
242    assert!(result.unwrap_err().contains("invalid format"));
243  }
244
245  #[test]
246  fn test_validate_pseudo_selector_invalid_attribute_unclosed_quotes() {
247    let result = validate_pseudo_selector("[data-state=\"open]");
248    assert!(result.is_err());
249    assert!(result.unwrap_err().contains("invalid format"));
250  }
251
252  #[test]
253  fn test_validate_pseudo_selector_invalid_attribute_nested_bracket() {
254    let result = validate_pseudo_selector("[data-[nested]=\"open\"]");
255    assert!(result.is_err());
256    assert!(result.unwrap_err().contains("invalid format"));
257  }
258
259  #[test]
260  fn test_ancestor_with_default_options() {
261    let result = ancestor(":hover", None).unwrap();
262    assert_eq!(result, ":where(.x-default-marker:hover *)");
263  }
264
265  #[test]
266  fn test_descendant_with_default_options() {
267    let result = descendant(":focus", None).unwrap();
268    assert_eq!(result, ":where(:has(.x-default-marker:focus))");
269  }
270
271  #[test]
272  fn test_sibling_before_with_default_options() {
273    let result = sibling_before(":hover", None).unwrap();
274    assert_eq!(result, ":where(.x-default-marker:hover ~ *)");
275  }
276
277  #[test]
278  fn test_sibling_after_with_default_options() {
279    let result = sibling_after(":focus", None).unwrap();
280    assert_eq!(result, ":where(:has(~ .x-default-marker:focus))");
281  }
282
283  #[test]
284  fn test_any_sibling_with_default_options() {
285    let result = any_sibling(":active", None).unwrap();
286    assert_eq!(
287      result,
288      ":where(.x-default-marker:active ~ *, :has(~ .x-default-marker:active))"
289    );
290  }
291
292  #[test]
293  fn test_with_custom_options() {
294    let options = StyleXStateOptions {
295      class_name_prefix: "custom".to_string(),
296      ..Default::default()
297    };
298    let result = ancestor(":hover", Some(&options)).unwrap();
299    assert_eq!(result, ":where(.custom-default-marker:hover *)");
300  }
301
302  #[test]
303  fn test_with_empty_prefix() {
304    let options = StyleXStateOptions {
305      class_name_prefix: "".to_string(),
306      ..Default::default()
307    };
308    let result = ancestor(":hover", Some(&options)).unwrap();
309    assert_eq!(result, ":where(.default-marker:hover *)");
310  }
311}