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
15fn 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
33fn 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 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
61fn 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 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 if !in_single_quote && !in_double_quote {
84 if c == '[' {
85 bracket_count += 1;
86 if bracket_count > 1 {
88 return false;
89 }
90 } else if c == ']' {
91 bracket_count -= 1;
92 if bracket_count < 0 || (bracket_count == 0 && i < chars.len() - 1) {
94 return false;
95 }
96 }
97 }
98 }
99
100 bracket_count == 0 && !in_single_quote && !in_double_quote
102}
103
104pub 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
121pub 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
138pub 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
158pub 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
175pub 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}