1use presentar_core::Widget;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum Selector {
13 Type(String),
15 Id(String),
17 TestId(String),
19 Class(String),
21 Attribute { name: String, value: String },
23 Descendant(Box<Selector>, Box<Selector>),
25 Child(Box<Selector>, Box<Selector>),
27}
28
29impl Selector {
30 pub fn parse(input: &str) -> Result<Self, SelectorError> {
36 SelectorParser::new(input).parse()
37 }
38
39 #[must_use]
41 pub fn matches(&self, widget: &dyn Widget) -> bool {
42 match self {
43 Self::Type(name) => {
44 name.is_empty()
47 }
48 Self::Id(_id) => {
49 false
51 }
52 Self::TestId(id) => Widget::test_id(widget) == Some(id.as_str()),
53 Self::Class(_class) => {
54 false
56 }
57 Self::Attribute { name, value } => {
58 if name == "data-testid" {
59 Widget::test_id(widget) == Some(value.as_str())
60 } else if name == "aria-label" {
61 widget.accessible_name() == Some(value.as_str())
62 } else {
63 false
64 }
65 }
66 Self::Descendant(_, _) | Self::Child(_, _) => {
67 false
69 }
70 }
71 }
72}
73
74pub struct SelectorParser<'a> {
76 input: &'a str,
77 pos: usize,
78}
79
80impl<'a> SelectorParser<'a> {
81 #[must_use]
83 pub const fn new(input: &'a str) -> Self {
84 Self { input, pos: 0 }
85 }
86
87 pub fn parse(&mut self) -> Result<Selector, SelectorError> {
89 self.skip_whitespace();
90
91 if self.input.is_empty() {
92 return Err(SelectorError::Empty);
93 }
94
95 self.parse_selector()
96 }
97
98 fn parse_selector(&mut self) -> Result<Selector, SelectorError> {
99 let first = self.peek_char().ok_or(SelectorError::Empty)?;
100
101 match first {
102 '#' => self.parse_id(),
103 '.' => self.parse_class(),
104 '[' => self.parse_attribute(),
105 _ if first.is_alphabetic() => self.parse_type(),
106 _ => Err(SelectorError::UnexpectedChar(first)),
107 }
108 }
109
110 fn parse_id(&mut self) -> Result<Selector, SelectorError> {
111 self.advance(); let id = self.read_identifier()?;
113 Ok(Selector::Id(id))
114 }
115
116 fn parse_class(&mut self) -> Result<Selector, SelectorError> {
117 self.advance(); let class = self.read_identifier()?;
119 Ok(Selector::Class(class))
120 }
121
122 fn parse_type(&mut self) -> Result<Selector, SelectorError> {
123 let name = self.read_identifier()?;
124 Ok(Selector::Type(name))
125 }
126
127 fn parse_attribute(&mut self) -> Result<Selector, SelectorError> {
128 self.advance(); let name = self.read_until('=');
131 if name.is_empty() {
132 return Err(SelectorError::InvalidAttribute);
133 }
134
135 self.advance(); let quote = self.peek_char();
139 if quote == Some('\'') || quote == Some('"') {
140 self.advance();
141 }
142
143 let value = self.read_until_any(&['\'', '"', ']']);
144
145 if self.peek_char() == Some('\'') || self.peek_char() == Some('"') {
147 self.advance();
148 }
149
150 if self.peek_char() != Some(']') {
152 return Err(SelectorError::UnclosedAttribute);
153 }
154 self.advance();
155
156 if name == "data-testid" {
158 Ok(Selector::TestId(value))
159 } else {
160 Ok(Selector::Attribute { name, value })
161 }
162 }
163
164 fn read_identifier(&mut self) -> Result<String, SelectorError> {
165 let start = self.pos;
166 while let Some(c) = self.peek_char() {
167 if c.is_alphanumeric() || c == '-' || c == '_' {
168 self.advance();
169 } else {
170 break;
171 }
172 }
173
174 if self.pos == start {
175 return Err(SelectorError::ExpectedIdentifier);
176 }
177
178 Ok(self.input[start..self.pos].to_string())
179 }
180
181 fn read_until(&mut self, stop: char) -> String {
182 let start = self.pos;
183 while let Some(c) = self.peek_char() {
184 if c == stop {
185 break;
186 }
187 self.advance();
188 }
189 self.input[start..self.pos].to_string()
190 }
191
192 fn read_until_any(&mut self, stops: &[char]) -> String {
193 let start = self.pos;
194 while let Some(c) = self.peek_char() {
195 if stops.contains(&c) {
196 break;
197 }
198 self.advance();
199 }
200 self.input[start..self.pos].to_string()
201 }
202
203 fn skip_whitespace(&mut self) {
204 while let Some(c) = self.peek_char() {
205 if c.is_whitespace() {
206 self.advance();
207 } else {
208 break;
209 }
210 }
211 }
212
213 fn peek_char(&self) -> Option<char> {
214 self.input[self.pos..].chars().next()
215 }
216
217 fn advance(&mut self) {
218 if let Some(c) = self.peek_char() {
219 self.pos += c.len_utf8();
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum SelectorError {
227 Empty,
229 UnexpectedChar(char),
231 ExpectedIdentifier,
233 InvalidAttribute,
235 UnclosedAttribute,
237}
238
239impl std::fmt::Display for SelectorError {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Self::Empty => write!(f, "empty selector"),
243 Self::UnexpectedChar(c) => write!(f, "unexpected character: '{c}'"),
244 Self::ExpectedIdentifier => write!(f, "expected identifier"),
245 Self::InvalidAttribute => write!(f, "invalid attribute syntax"),
246 Self::UnclosedAttribute => write!(f, "unclosed attribute bracket"),
247 }
248 }
249}
250
251impl std::error::Error for SelectorError {}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_parse_type() {
259 let sel = Selector::parse("Button").unwrap();
260 assert_eq!(sel, Selector::Type("Button".to_string()));
261 }
262
263 #[test]
264 fn test_parse_id() {
265 let sel = Selector::parse("#submit-btn").unwrap();
266 assert_eq!(sel, Selector::Id("submit-btn".to_string()));
267 }
268
269 #[test]
270 fn test_parse_class() {
271 let sel = Selector::parse(".primary").unwrap();
272 assert_eq!(sel, Selector::Class("primary".to_string()));
273 }
274
275 #[test]
276 fn test_parse_test_id() {
277 let sel = Selector::parse("[data-testid='login']").unwrap();
278 assert_eq!(sel, Selector::TestId("login".to_string()));
279 }
280
281 #[test]
282 fn test_parse_test_id_double_quotes() {
283 let sel = Selector::parse("[data-testid=\"login\"]").unwrap();
284 assert_eq!(sel, Selector::TestId("login".to_string()));
285 }
286
287 #[test]
288 fn test_parse_attribute() {
289 let sel = Selector::parse("[aria-label='Close']").unwrap();
290 assert_eq!(
291 sel,
292 Selector::Attribute {
293 name: "aria-label".to_string(),
294 value: "Close".to_string(),
295 }
296 );
297 }
298
299 #[test]
300 fn test_parse_empty_error() {
301 let result = Selector::parse("");
302 assert_eq!(result, Err(SelectorError::Empty));
303 }
304
305 #[test]
306 fn test_parse_whitespace() {
307 let sel = Selector::parse(" Button ").unwrap();
308 assert_eq!(sel, Selector::Type("Button".to_string()));
309 }
310
311 #[test]
312 fn test_selector_error_display() {
313 assert_eq!(SelectorError::Empty.to_string(), "empty selector");
314 assert_eq!(
315 SelectorError::UnexpectedChar('@').to_string(),
316 "unexpected character: '@'"
317 );
318 }
319
320 #[test]
325 fn test_selector_error_display_all_variants() {
326 assert_eq!(SelectorError::Empty.to_string(), "empty selector");
327 assert_eq!(
328 SelectorError::ExpectedIdentifier.to_string(),
329 "expected identifier"
330 );
331 assert_eq!(
332 SelectorError::InvalidAttribute.to_string(),
333 "invalid attribute syntax"
334 );
335 assert_eq!(
336 SelectorError::UnclosedAttribute.to_string(),
337 "unclosed attribute bracket"
338 );
339 }
340
341 #[test]
346 fn test_parse_type_with_hyphen() {
347 let sel = Selector::parse("data-table").unwrap();
348 assert_eq!(sel, Selector::Type("data-table".to_string()));
349 }
350
351 #[test]
352 fn test_parse_type_with_underscore() {
353 let sel = Selector::parse("my_widget").unwrap();
354 assert_eq!(sel, Selector::Type("my_widget".to_string()));
355 }
356
357 #[test]
358 fn test_parse_type_with_numbers() {
359 let sel = Selector::parse("Button2").unwrap();
360 assert_eq!(sel, Selector::Type("Button2".to_string()));
361 }
362
363 #[test]
364 fn test_parse_type_case_sensitive() {
365 let sel1 = Selector::parse("Button").unwrap();
366 let sel2 = Selector::parse("button").unwrap();
367 assert_ne!(sel1, sel2);
368 }
369
370 #[test]
375 fn test_parse_id_with_numbers() {
376 let sel = Selector::parse("#item-123").unwrap();
377 assert_eq!(sel, Selector::Id("item-123".to_string()));
378 }
379
380 #[test]
381 fn test_parse_id_with_underscores() {
382 let sel = Selector::parse("#my_element").unwrap();
383 assert_eq!(sel, Selector::Id("my_element".to_string()));
384 }
385
386 #[test]
387 fn test_parse_id_simple() {
388 let sel = Selector::parse("#main").unwrap();
389 assert_eq!(sel, Selector::Id("main".to_string()));
390 }
391
392 #[test]
397 fn test_parse_class_with_numbers() {
398 let sel = Selector::parse(".col-12").unwrap();
399 assert_eq!(sel, Selector::Class("col-12".to_string()));
400 }
401
402 #[test]
403 fn test_parse_class_with_underscores() {
404 let sel = Selector::parse(".btn_primary").unwrap();
405 assert_eq!(sel, Selector::Class("btn_primary".to_string()));
406 }
407
408 #[test]
413 fn test_parse_attribute_role() {
414 let sel = Selector::parse("[role='button']").unwrap();
415 assert_eq!(
416 sel,
417 Selector::Attribute {
418 name: "role".to_string(),
419 value: "button".to_string(),
420 }
421 );
422 }
423
424 #[test]
425 fn test_parse_attribute_disabled() {
426 let sel = Selector::parse("[disabled='true']").unwrap();
427 assert_eq!(
428 sel,
429 Selector::Attribute {
430 name: "disabled".to_string(),
431 value: "true".to_string(),
432 }
433 );
434 }
435
436 #[test]
437 fn test_parse_attribute_with_spaces_in_value() {
438 let sel = Selector::parse("[aria-label='Click to submit']").unwrap();
439 assert_eq!(
440 sel,
441 Selector::Attribute {
442 name: "aria-label".to_string(),
443 value: "Click to submit".to_string(),
444 }
445 );
446 }
447
448 #[test]
449 fn test_parse_testid_variations() {
450 let sel1 = Selector::parse("[data-testid='foo']").unwrap();
452 assert_eq!(sel1, Selector::TestId("foo".to_string()));
453
454 let sel2 = Selector::parse("[data-testid=\"bar\"]").unwrap();
456 assert_eq!(sel2, Selector::TestId("bar".to_string()));
457 }
458
459 #[test]
464 fn test_parse_unexpected_char() {
465 let result = Selector::parse("@invalid");
466 assert_eq!(result, Err(SelectorError::UnexpectedChar('@')));
467 }
468
469 #[test]
470 fn test_parse_unexpected_char_special() {
471 assert!(Selector::parse("!invalid").is_err());
472 assert!(Selector::parse("$invalid").is_err());
473 assert!(Selector::parse("*invalid").is_err());
474 }
475
476 #[test]
477 fn test_parse_empty_id() {
478 let result = Selector::parse("#");
479 assert_eq!(result, Err(SelectorError::ExpectedIdentifier));
480 }
481
482 #[test]
483 fn test_parse_empty_class() {
484 let result = Selector::parse(".");
485 assert_eq!(result, Err(SelectorError::ExpectedIdentifier));
486 }
487
488 #[test]
489 fn test_parse_unclosed_attribute() {
490 let result = Selector::parse("[data-testid='foo'");
491 assert_eq!(result, Err(SelectorError::UnclosedAttribute));
492 }
493
494 #[test]
495 fn test_parse_invalid_attribute_no_equals() {
496 let result = Selector::parse("[disabled]");
497 assert!(result.is_err());
499 }
500
501 #[test]
506 fn test_parse_leading_whitespace() {
507 let sel = Selector::parse(" #main").unwrap();
508 assert_eq!(sel, Selector::Id("main".to_string()));
509 }
510
511 #[test]
512 fn test_parse_trailing_whitespace() {
513 let sel = Selector::parse(".button ").unwrap();
514 assert_eq!(sel, Selector::Class("button".to_string()));
515 }
516
517 #[test]
518 fn test_parse_only_whitespace() {
519 let result = Selector::parse(" ");
520 assert_eq!(result, Err(SelectorError::Empty));
521 }
522
523 #[test]
528 fn test_selector_equality() {
529 let sel1 = Selector::parse("#main").unwrap();
530 let sel2 = Selector::parse("#main").unwrap();
531 assert_eq!(sel1, sel2);
532 }
533
534 #[test]
535 fn test_selector_inequality_different_types() {
536 let id = Selector::parse("#main").unwrap();
537 let class = Selector::parse(".main").unwrap();
538 assert_ne!(id, class);
539 }
540
541 #[test]
542 fn test_selector_inequality_different_values() {
543 let sel1 = Selector::parse("#main").unwrap();
544 let sel2 = Selector::parse("#header").unwrap();
545 assert_ne!(sel1, sel2);
546 }
547
548 #[test]
553 fn test_selector_clone() {
554 let sel = Selector::parse("[data-testid='foo']").unwrap();
555 let cloned = sel.clone();
556 assert_eq!(sel, cloned);
557 }
558
559 #[test]
564 fn test_parser_new() {
565 let parser = SelectorParser::new("Button");
566 assert_eq!(parser.input, "Button");
567 assert_eq!(parser.pos, 0);
568 }
569
570 #[test]
575 fn test_selector_descendant_structure() {
576 let parent = Box::new(Selector::Type("Container".to_string()));
578 let child = Box::new(Selector::Type("Button".to_string()));
579 let desc = Selector::Descendant(parent, child);
580
581 matches!(desc, Selector::Descendant(_, _));
583 }
584
585 #[test]
586 fn test_selector_child_structure() {
587 let parent = Box::new(Selector::Type("Row".to_string()));
589 let child = Box::new(Selector::Type("Column".to_string()));
590 let sel = Selector::Child(parent, child);
591
592 matches!(sel, Selector::Child(_, _));
594 }
595
596 #[test]
601 fn test_selector_debug_format() {
602 let sel = Selector::parse("#main").unwrap();
603 let debug = format!("{:?}", sel);
604 assert!(debug.contains("Id"));
605 assert!(debug.contains("main"));
606 }
607
608 #[test]
609 fn test_selector_error_debug_format() {
610 let err = SelectorError::Empty;
611 let debug = format!("{:?}", err);
612 assert!(debug.contains("Empty"));
613 }
614
615 #[test]
620 fn test_parse_unicode_in_attribute_value() {
621 let sel = Selector::parse("[aria-label='日本語']").unwrap();
622 assert_eq!(
623 sel,
624 Selector::Attribute {
625 name: "aria-label".to_string(),
626 value: "日本語".to_string(),
627 }
628 );
629 }
630
631 #[test]
632 fn test_parse_emoji_in_attribute_value() {
633 let sel = Selector::parse("[aria-label='Hello 👋']").unwrap();
634 assert_eq!(
635 sel,
636 Selector::Attribute {
637 name: "aria-label".to_string(),
638 value: "Hello 👋".to_string(),
639 }
640 );
641 }
642}