1use std::iter;
4
5use cssparser::{ParseError, Parser, Token};
6
7use crate::css::types::PseudoClass;
8use crate::widget::context::AppContext;
9use crate::widget::WidgetId;
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum Selector {
14 Type(String),
16 Class(String),
18 Id(String),
20 Universal,
22 PseudoClass(PseudoClass),
24 Descendant(Box<Selector>, Box<Selector>),
26 Child(Box<Selector>, Box<Selector>),
28 Compound(Vec<Selector>),
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34pub struct Specificity(pub u32, pub u32, pub u32);
35
36impl Selector {
37 pub fn specificity(&self) -> Specificity {
39 match self {
40 Selector::Id(_) => Specificity(1, 0, 0),
41 Selector::Class(_) => Specificity(0, 1, 0),
42 Selector::PseudoClass(_) => Specificity(0, 1, 0),
43 Selector::Type(_) => Specificity(0, 0, 1),
44 Selector::Universal => Specificity(0, 0, 0),
45 Selector::Compound(parts) => {
46 let mut total = Specificity(0, 0, 0);
47 for s in parts {
48 let Specificity(a, b, c) = s.specificity();
49 total = Specificity(total.0 + a, total.1 + b, total.2 + c);
50 }
51 total
52 }
53 Selector::Descendant(left, right) => {
54 let Specificity(a1, b1, c1) = left.specificity();
55 let Specificity(a2, b2, c2) = right.specificity();
56 Specificity(a1 + a2, b1 + b2, c1 + c2)
57 }
58 Selector::Child(left, right) => {
59 let Specificity(a1, b1, c1) = left.specificity();
60 let Specificity(a2, b2, c2) = right.specificity();
61 Specificity(a1 + a2, b1 + b2, c1 + c2)
62 }
63 }
64 }
65}
66
67fn ancestors(id: WidgetId, ctx: &AppContext) -> impl Iterator<Item = WidgetId> + '_ {
69 iter::successors(ctx.parent.get(id).and_then(|p| *p), move |&cur| {
70 ctx.parent.get(cur).and_then(|p| *p)
71 })
72}
73
74pub fn selector_matches(sel: &Selector, id: WidgetId, ctx: &AppContext) -> bool {
76 match sel {
77 Selector::Universal => true,
78 Selector::Type(name) => ctx
79 .arena
80 .get(id)
81 .is_some_and(|w| w.widget_type_name() == name.as_str()),
82 Selector::Class(cls) => ctx
83 .arena
84 .get(id)
85 .is_some_and(|w| w.classes().contains(&cls.as_str())),
86 Selector::Id(expected_id) => ctx
87 .arena
88 .get(id)
89 .is_some_and(|w| w.id() == Some(expected_id.as_str())),
90 Selector::PseudoClass(pc) => ctx
91 .pseudo_classes
92 .get(id)
93 .is_some_and(|set| set.contains(pc)),
94 Selector::Compound(parts) => parts.iter().all(|s| selector_matches(s, id, ctx)),
95 Selector::Descendant(ancestor_sel, subject_sel) => {
96 if !selector_matches(subject_sel, id, ctx) {
97 return false;
98 }
99 ancestors(id, ctx).any(|anc_id| selector_matches(ancestor_sel, anc_id, ctx))
100 }
101 Selector::Child(parent_sel, subject_sel) => {
102 if !selector_matches(subject_sel, id, ctx) {
103 return false;
104 }
105 ctx.parent
107 .get(id)
108 .and_then(|p| *p)
109 .is_some_and(|parent_id| selector_matches(parent_sel, parent_id, ctx))
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct SelectorParseError(pub String);
117
118fn parse_pseudo_class_name(name: &str) -> Option<PseudoClass> {
120 match name {
121 "focus" => Some(PseudoClass::Focus),
122 "hover" => Some(PseudoClass::Hover),
123 "disabled" => Some(PseudoClass::Disabled),
124 _ => None,
125 }
126}
127
128fn parse_compound_selector_tokens<'i>(
130 input: &mut Parser<'i, '_>,
131) -> Result<Option<Selector>, ParseError<'i, SelectorParseError>> {
132 let mut simples: Vec<Selector> = Vec::new();
133
134 loop {
135 let state = input.state();
136 let location = input.current_source_location();
137 match input.next_including_whitespace() {
138 Ok(Token::Ident(name)) => {
139 simples.push(Selector::Type(name.to_string()));
140 }
141 Ok(Token::Delim('*')) => {
142 simples.push(Selector::Universal);
143 }
144 Ok(Token::Delim('.')) => {
145 let class_name = input.expect_ident_cloned().map_err(|_| {
146 location.new_custom_error(SelectorParseError(
147 "expected class name after '.'".to_string(),
148 ))
149 })?;
150 simples.push(Selector::Class(class_name.to_string()));
151 }
152 Ok(Token::IDHash(id_val)) => {
153 simples.push(Selector::Id(id_val.to_string()));
154 }
155 Ok(Token::Colon) => {
156 let pc_name = input.expect_ident_cloned().map_err(|_| {
157 location.new_custom_error(SelectorParseError(
158 "expected pseudo-class name after ':'".to_string(),
159 ))
160 })?;
161 match parse_pseudo_class_name(pc_name.as_ref()) {
162 Some(pc) => simples.push(Selector::PseudoClass(pc)),
163 None => {
164 return Err(location.new_custom_error(SelectorParseError(format!(
165 "unknown pseudo-class: {}",
166 pc_name
167 ))));
168 }
169 }
170 }
171 Ok(Token::WhiteSpace(_)) | Ok(Token::Comma) | Ok(Token::Delim('>')) | Err(_) => {
173 input.reset(&state);
174 break;
175 }
176 _ => {
177 input.reset(&state);
178 break;
179 }
180 }
181 }
182
183 if simples.is_empty() {
184 return Ok(None);
185 }
186 if simples.len() == 1 {
187 Ok(Some(simples.remove(0)))
188 } else {
189 Ok(Some(Selector::Compound(simples)))
190 }
191}
192
193fn parse_single_selector<'i>(
195 input: &mut Parser<'i, '_>,
196) -> Result<Option<Selector>, ParseError<'i, SelectorParseError>> {
197 input.skip_whitespace();
199
200 let first = match parse_compound_selector_tokens(input)? {
201 Some(s) => s,
202 None => return Ok(None),
203 };
204
205 let mut current = first;
206
207 loop {
208 let state = input.state();
210 match input.next_including_whitespace() {
211 Ok(Token::WhiteSpace(_)) => {
212 input.skip_whitespace();
215 let state2 = input.state();
216 match input.next_including_whitespace() {
217 Ok(Token::Delim('>')) => {
218 input.skip_whitespace();
220 match parse_compound_selector_tokens(input)? {
221 Some(right) => {
222 current = Selector::Child(Box::new(current), Box::new(right));
223 }
224 None => break,
225 }
226 }
227 Ok(Token::Comma) | Err(_) => {
228 input.reset(&state2);
229 break;
230 }
231 _ => {
232 input.reset(&state2);
234 match parse_compound_selector_tokens(input)? {
235 Some(right) => {
236 current = Selector::Descendant(Box::new(current), Box::new(right));
237 }
238 None => break,
239 }
240 }
241 }
242 }
243 Ok(Token::Delim('>')) => {
244 input.skip_whitespace();
246 match parse_compound_selector_tokens(input)? {
247 Some(right) => {
248 current = Selector::Child(Box::new(current), Box::new(right));
249 }
250 None => break,
251 }
252 }
253 Ok(Token::Comma) | Err(_) => {
254 input.reset(&state);
255 break;
256 }
257 _ => {
258 input.reset(&state);
259 break;
260 }
261 }
262 }
263
264 Ok(Some(current))
265}
266
267pub struct SelectorParser;
269
270impl SelectorParser {
271 pub fn parse_selector_list<'i>(
273 input: &mut Parser<'i, '_>,
274 ) -> Result<Vec<Selector>, ParseError<'i, SelectorParseError>> {
275 let mut selectors = Vec::new();
276
277 loop {
278 input.skip_whitespace();
279 if input.is_exhausted() {
280 break;
281 }
282
283 match parse_single_selector(input)? {
284 Some(sel) => selectors.push(sel),
285 None => break,
286 }
287
288 input.skip_whitespace();
289 if input.is_exhausted() {
290 break;
291 }
292
293 let state = input.state();
295 match input.next() {
296 Ok(Token::Comma) => {
297 }
299 _ => {
300 input.reset(&state);
301 break;
302 }
303 }
304 }
305
306 Ok(selectors)
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::css::types::{ComputedStyle, PseudoClassSet};
314 use crate::widget::context::AppContext;
315 use ratatui::{buffer::Buffer, layout::Rect};
316
317 fn parse_selector(input: &str) -> Vec<Selector> {
318 let mut parser_input = cssparser::ParserInput::new(input);
319 let mut parser = cssparser::Parser::new(&mut parser_input);
320 SelectorParser::parse_selector_list(&mut parser).expect("parse failed")
321 }
322
323 struct TypeWidget(&'static str);
325 impl crate::widget::Widget for TypeWidget {
326 fn render(&self, _: &AppContext, _: Rect, _: &mut Buffer) {}
327 fn widget_type_name(&self) -> &'static str {
328 self.0
329 }
330 }
331
332 fn make_single_widget_ctx(w: Box<dyn crate::widget::Widget>) -> (AppContext, WidgetId) {
333 let mut ctx = AppContext::new();
334 let id = ctx.arena.insert(w);
335 ctx.parent.insert(id, None);
336 ctx.pseudo_classes.insert(id, PseudoClassSet::default());
337 ctx.computed_styles.insert(id, ComputedStyle::default());
338 ctx.inline_styles.insert(id, Vec::new());
339 (ctx, id)
340 }
341
342 #[test]
343 fn parse_type_selector() {
344 let sels = parse_selector("Button");
345 assert_eq!(sels, vec![Selector::Type("Button".to_string())]);
346 }
347
348 #[test]
349 fn parse_class_selector() {
350 let sels = parse_selector(".highlight");
351 assert_eq!(sels, vec![Selector::Class("highlight".to_string())]);
352 }
353
354 #[test]
355 fn parse_id_selector() {
356 let sels = parse_selector("#sidebar");
357 assert_eq!(sels, vec![Selector::Id("sidebar".to_string())]);
358 }
359
360 #[test]
361 fn parse_compound_selector_type_class() {
362 let sels = parse_selector("Button.active");
363 assert_eq!(
364 sels,
365 vec![Selector::Compound(vec![
366 Selector::Type("Button".to_string()),
367 Selector::Class("active".to_string()),
368 ])]
369 );
370 }
371
372 #[test]
373 fn parse_descendant_selector() {
374 let sels = parse_selector("Screen Button");
375 assert_eq!(
376 sels,
377 vec![Selector::Descendant(
378 Box::new(Selector::Type("Screen".to_string())),
379 Box::new(Selector::Type("Button".to_string())),
380 )]
381 );
382 }
383
384 #[test]
385 fn parse_child_selector() {
386 let sels = parse_selector("Container > Button");
387 assert_eq!(
388 sels,
389 vec![Selector::Child(
390 Box::new(Selector::Type("Container".to_string())),
391 Box::new(Selector::Type("Button".to_string())),
392 )]
393 );
394 }
395
396 #[test]
397 fn parse_pseudo_class_selector() {
398 let sels = parse_selector("Button:focus");
399 assert_eq!(
400 sels,
401 vec![Selector::Compound(vec![
402 Selector::Type("Button".to_string()),
403 Selector::PseudoClass(PseudoClass::Focus),
404 ])]
405 );
406 }
407
408 #[test]
409 fn parse_universal_selector() {
410 let sels = parse_selector("*");
411 assert_eq!(sels, vec![Selector::Universal]);
412 }
413
414 #[test]
415 fn selector_matches_type_correct() {
416 let (ctx, id) = make_single_widget_ctx(Box::new(TypeWidget("Button")));
417 assert!(selector_matches(
418 &Selector::Type("Button".to_string()),
419 id,
420 &ctx
421 ));
422 }
423
424 #[test]
425 fn selector_matches_type_wrong() {
426 let (ctx, id) = make_single_widget_ctx(Box::new(TypeWidget("Label")));
427 assert!(!selector_matches(
428 &Selector::Type("Button".to_string()),
429 id,
430 &ctx
431 ));
432 }
433
434 #[test]
435 fn selector_matches_descendant() {
436 let mut ctx = AppContext::new();
437 let screen = ctx
438 .arena
439 .insert(Box::new(TypeWidget("Screen")) as Box<dyn crate::widget::Widget>);
440 let button = ctx
441 .arena
442 .insert(Box::new(TypeWidget("Button")) as Box<dyn crate::widget::Widget>);
443 ctx.parent.insert(screen, None);
444 ctx.parent.insert(button, Some(screen));
445 ctx.pseudo_classes.insert(screen, PseudoClassSet::default());
446 ctx.pseudo_classes.insert(button, PseudoClassSet::default());
447 ctx.children.insert(screen, vec![button]);
448
449 let sel = Selector::Descendant(
450 Box::new(Selector::Type("Screen".to_string())),
451 Box::new(Selector::Type("Button".to_string())),
452 );
453 assert!(selector_matches(&sel, button, &ctx));
454 }
455
456 #[test]
457 fn selector_matches_child_non_parent_returns_false() {
458 let mut ctx = AppContext::new();
460 let screen = ctx
461 .arena
462 .insert(Box::new(TypeWidget("Screen")) as Box<dyn crate::widget::Widget>);
463 let container = ctx
464 .arena
465 .insert(Box::new(TypeWidget("Container")) as Box<dyn crate::widget::Widget>);
466 let button = ctx
467 .arena
468 .insert(Box::new(TypeWidget("Button")) as Box<dyn crate::widget::Widget>);
469 ctx.parent.insert(screen, None);
470 ctx.parent.insert(container, Some(screen));
471 ctx.parent.insert(button, Some(container));
472 ctx.pseudo_classes.insert(screen, PseudoClassSet::default());
473 ctx.pseudo_classes
474 .insert(container, PseudoClassSet::default());
475 ctx.pseudo_classes.insert(button, PseudoClassSet::default());
476
477 let sel = Selector::Child(
478 Box::new(Selector::Type("Screen".to_string())),
479 Box::new(Selector::Type("Button".to_string())),
480 );
481 assert!(!selector_matches(&sel, button, &ctx));
483 }
484
485 #[test]
486 fn selector_matches_pseudo_class_focus() {
487 let mut ctx = AppContext::new();
488 let btn = ctx
489 .arena
490 .insert(Box::new(TypeWidget("Button")) as Box<dyn crate::widget::Widget>);
491 ctx.parent.insert(btn, None);
492 let mut pcs = PseudoClassSet::default();
493 pcs.insert(PseudoClass::Focus);
494 ctx.pseudo_classes.insert(btn, pcs);
495
496 assert!(selector_matches(
497 &Selector::PseudoClass(PseudoClass::Focus),
498 btn,
499 &ctx
500 ));
501 }
502
503 #[test]
504 fn specificity_ordering() {
505 let type_spec = Selector::Type("Button".to_string()).specificity();
506 let class_spec = Selector::Class("active".to_string()).specificity();
507 let id_spec = Selector::Id("main".to_string()).specificity();
508
509 assert!(
510 type_spec < class_spec,
511 "type should have lower specificity than class"
512 );
513 assert!(
514 class_spec < id_spec,
515 "class should have lower specificity than id"
516 );
517 assert_eq!(type_spec, Specificity(0, 0, 1));
518 assert_eq!(class_spec, Specificity(0, 1, 0));
519 assert_eq!(id_spec, Specificity(1, 0, 0));
520 }
521}