1use sup_xml_tree::dom::Node as ArenaNode;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Selector {
39 parts: Vec<Part>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43struct Part {
44 sel: SimpleSelector,
45 combinator_to_left: Combinator,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum Combinator {
51 Start,
52 Descendant,
53 Child,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Default)]
57struct SimpleSelector {
58 tag: Option<String>,
60 id: Option<String>,
61 classes: Vec<String>,
62 attrs: Vec<AttrFilter>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66enum AttrFilter {
67 Has(String),
68 Equals(String, String),
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ParseSelectorError {
74 message: String,
75}
76
77impl std::fmt::Display for ParseSelectorError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "invalid selector: {}", self.message)
80 }
81}
82
83impl std::error::Error for ParseSelectorError {}
84
85impl Selector {
86 pub fn parse(input: &str) -> Result<Self, ParseSelectorError> {
88 let mut p = Parser { input, pos: 0 };
89 p.skip_ws();
90 let parts = p.parse_chain()?;
91 if parts.is_empty() {
92 return Err(p.err("empty selector"));
93 }
94 Ok(Selector { parts })
95 }
96
97 pub fn matches(&self, popped: &ArenaNode<'_>, stack: &[&ArenaNode<'_>]) -> bool {
106 let n = self.parts.len();
107 if n == 0 || !self.parts[n - 1].sel.matches(popped) {
108 return false;
109 }
110 if n == 1 {
111 return true;
112 }
113 self.match_left(n - 1, stack.len(), stack)
114 }
115
116 fn match_left(&self, right_idx: usize, cursor: usize, stack: &[&ArenaNode<'_>]) -> bool {
117 let comb = self.parts[right_idx].combinator_to_left;
118 let left_idx = right_idx - 1;
119 let left_sel = &self.parts[left_idx].sel;
120 match comb {
121 Combinator::Child => {
122 if cursor == 0 { return false; }
123 let pos = cursor - 1;
124 if !left_sel.matches(stack[pos]) { return false; }
125 if left_idx == 0 { return true; }
126 self.match_left(left_idx, pos, stack)
127 }
128 Combinator::Descendant => {
129 for pos in (0..cursor).rev() {
130 if !left_sel.matches(stack[pos]) { continue; }
131 if left_idx == 0 { return true; }
132 if self.match_left(left_idx, pos, stack) { return true; }
133 }
134 false
135 }
136 Combinator::Start => unreachable!("Start only at parts[0]"),
137 }
138 }
139}
140
141impl SimpleSelector {
142 fn matches(&self, el: &ArenaNode<'_>) -> bool {
145 if !el.is_element() { return false; }
146 if let Some(t) = &self.tag {
147 if el.name() != t.as_str() { return false; }
148 }
149 if let Some(id) = &self.id {
150 let has = el.attributes().any(|a| a.name() == "id" && a.value() == *id);
151 if !has { return false; }
152 }
153 for class in &self.classes {
154 let has = el.attributes().any(|a| {
155 a.name() == "class"
156 && a.value().split_ascii_whitespace().any(|tok| tok == class)
157 });
158 if !has { return false; }
159 }
160 for filter in &self.attrs {
161 let ok = match filter {
162 AttrFilter::Has(n) => el.attributes().any(|a| a.name() == n.as_str()),
163 AttrFilter::Equals(n, v) => el.attributes().any(|a| a.name() == n.as_str() && a.value() == *v),
164 };
165 if !ok { return false; }
166 }
167 true
168 }
169}
170
171struct Parser<'a> {
174 input: &'a str,
175 pos: usize,
176}
177
178impl<'a> Parser<'a> {
179 fn err(&self, msg: impl Into<String>) -> ParseSelectorError {
180 ParseSelectorError { message: format!("{} (at offset {})", msg.into(), self.pos) }
181 }
182
183 fn peek(&self) -> Option<char> { self.input[self.pos..].chars().next() }
184
185 fn bump(&mut self) -> Option<char> {
186 let c = self.peek()?;
187 self.pos += c.len_utf8();
188 Some(c)
189 }
190
191 fn skip_ws(&mut self) -> bool {
194 let start = self.pos;
195 while let Some(c) = self.peek() {
196 if c.is_whitespace() { self.bump(); } else { break; }
197 }
198 self.pos != start
199 }
200
201 fn parse_chain(&mut self) -> Result<Vec<Part>, ParseSelectorError> {
202 let mut parts = Vec::new();
203 if self.peek().is_none() {
204 return Ok(parts);
205 }
206 let sel = self.parse_simple()?;
207 parts.push(Part { sel, combinator_to_left: Combinator::Start });
208 loop {
209 let had_ws = self.skip_ws();
210 match self.peek() {
211 None => break,
212 Some('>') => {
213 self.bump();
214 self.skip_ws();
215 let sel = self.parse_simple()?;
216 parts.push(Part { sel, combinator_to_left: Combinator::Child });
217 }
218 Some(_) => {
219 if !had_ws {
220 return Err(self.err("expected combinator (` ` or `>`) or end of selector"));
221 }
222 let sel = self.parse_simple()?;
223 parts.push(Part { sel, combinator_to_left: Combinator::Descendant });
224 }
225 }
226 }
227 Ok(parts)
228 }
229
230 fn parse_simple(&mut self) -> Result<SimpleSelector, ParseSelectorError> {
231 let mut sel = SimpleSelector::default();
232 let mut had_prefix = false; match self.peek() {
234 Some('*') => { self.bump(); had_prefix = true; }
235 Some(c) if is_ident_start(c) => {
236 sel.tag = Some(self.parse_ident()?);
237 had_prefix = true;
238 }
239 _ => {}
240 }
241 loop {
242 match self.peek() {
243 Some('.') => {
244 self.bump();
245 sel.classes.push(self.parse_ident()?);
246 }
247 Some('#') => {
248 self.bump();
249 if sel.id.is_some() {
250 return Err(self.err("multiple #id selectors"));
251 }
252 sel.id = Some(self.parse_ident()?);
253 }
254 Some('[') => {
255 self.bump();
256 sel.attrs.push(self.parse_attr_filter()?);
257 }
258 _ => break,
259 }
260 }
261 if !had_prefix && sel.id.is_none() && sel.classes.is_empty() && sel.attrs.is_empty() {
262 return Err(self.err("expected element name, `*`, `.class`, `#id`, or `[attr]`"));
263 }
264 Ok(sel)
265 }
266
267 fn parse_ident(&mut self) -> Result<String, ParseSelectorError> {
268 let start = self.pos;
269 while let Some(c) = self.peek() {
270 if is_ident_continue(c) { self.bump(); } else { break; }
271 }
272 if start == self.pos {
273 return Err(self.err("expected identifier"));
274 }
275 Ok(self.input[start..self.pos].to_owned())
276 }
277
278 fn parse_attr_filter(&mut self) -> Result<AttrFilter, ParseSelectorError> {
279 self.skip_ws();
280 let name = self.parse_ident()?;
281 self.skip_ws();
282 match self.peek() {
283 Some(']') => { self.bump(); Ok(AttrFilter::Has(name)) }
284 Some('=') => {
285 self.bump();
286 self.skip_ws();
287 let value = self.parse_attr_value()?;
288 self.skip_ws();
289 if self.peek() != Some(']') {
290 return Err(self.err("expected `]`"));
291 }
292 self.bump();
293 Ok(AttrFilter::Equals(name, value))
294 }
295 _ => Err(self.err("expected `=` or `]` after attribute name")),
296 }
297 }
298
299 fn parse_attr_value(&mut self) -> Result<String, ParseSelectorError> {
300 let quote = match self.peek() {
301 Some('"') | Some('\'') => self.bump().unwrap(),
302 _ => return self.parse_ident(),
303 };
304 let start = self.pos;
305 while let Some(c) = self.peek() {
306 if c == quote { break; }
307 self.bump();
308 }
309 if self.peek() != Some(quote) {
310 return Err(self.err("unterminated attribute value"));
311 }
312 let value = self.input[start..self.pos].to_owned();
313 self.bump();
314 Ok(value)
315 }
316}
317
318fn is_ident_start(c: char) -> bool {
319 c == '_' || c.is_alphabetic()
320}
321
322fn is_ident_continue(c: char) -> bool {
323 c == '_' || c == '-' || c == ':' || c.is_alphanumeric()
327}
328
329#[cfg(test)]
332mod tests {
333 use super::*;
334 use sup_xml_tree::dom::{DocumentBuilder, Node};
335
336 fn make_el<'a>(b: &'a DocumentBuilder, name: &str, attrs: &[(&str, &str)]) -> &'a Node<'a> {
338 let n = b.alloc_str(name);
339 let el = b.new_element(n);
340 for (an, av) in attrs {
341 let attr = b.new_attribute(b.alloc_str(an), b.alloc_str(av));
342 b.append_attribute(el, attr);
343 }
344 el
345 }
346
347 #[test]
350 fn parse_simple_tag() {
351 let s = Selector::parse("foo").unwrap();
352 assert_eq!(s.parts.len(), 1);
353 assert_eq!(s.parts[0].sel.tag.as_deref(), Some("foo"));
354 }
355
356 #[test]
357 fn parse_wildcard() {
358 let s = Selector::parse("*").unwrap();
359 assert_eq!(s.parts[0].sel.tag, None);
360 }
361
362 #[test]
363 fn parse_class_id_attrs() {
364 let s = Selector::parse(r#"div.foo.bar#main[hidden][type="x"]"#).unwrap();
365 let p = &s.parts[0].sel;
366 assert_eq!(p.tag.as_deref(), Some("div"));
367 assert_eq!(p.id.as_deref(), Some("main"));
368 assert_eq!(p.classes, vec!["foo".to_string(), "bar".into()]);
369 assert_eq!(p.attrs, vec![
370 AttrFilter::Has("hidden".into()),
371 AttrFilter::Equals("type".into(), "x".into()),
372 ]);
373 }
374
375 #[test]
376 fn parse_child_combinator() {
377 let s = Selector::parse("a > b > c").unwrap();
378 assert_eq!(s.parts.len(), 3);
379 assert_eq!(s.parts[0].combinator_to_left, Combinator::Start);
380 assert_eq!(s.parts[1].combinator_to_left, Combinator::Child);
381 assert_eq!(s.parts[2].combinator_to_left, Combinator::Child);
382 }
383
384 #[test]
385 fn parse_descendant_combinator() {
386 let s = Selector::parse("a b").unwrap();
387 assert_eq!(s.parts.len(), 2);
388 assert_eq!(s.parts[1].combinator_to_left, Combinator::Descendant);
389 }
390
391 #[test]
392 fn parse_mixed_combinators() {
393 let s = Selector::parse("a b > c").unwrap();
394 assert_eq!(s.parts[1].combinator_to_left, Combinator::Descendant);
395 assert_eq!(s.parts[2].combinator_to_left, Combinator::Child);
396 }
397
398 #[test]
399 fn parse_no_space_around_child_combinator() {
400 let s = Selector::parse("a>b").unwrap();
401 assert_eq!(s.parts.len(), 2);
402 assert_eq!(s.parts[1].combinator_to_left, Combinator::Child);
403 }
404
405 #[test]
406 fn parse_namespaced_name_with_colon() {
407 let s = Selector::parse("atom:entry").unwrap();
408 assert_eq!(s.parts[0].sel.tag.as_deref(), Some("atom:entry"));
409 }
410
411 #[test]
412 fn parse_unquoted_attr_value() {
413 let s = Selector::parse("[rel=next]").unwrap();
414 assert_eq!(s.parts[0].sel.attrs[0], AttrFilter::Equals("rel".into(), "next".into()));
415 }
416
417 #[test]
418 fn parse_single_quoted_attr_value() {
419 let s = Selector::parse("[a='hello world']").unwrap();
420 assert_eq!(s.parts[0].sel.attrs[0], AttrFilter::Equals("a".into(), "hello world".into()));
421 }
422
423 #[test]
424 fn parse_errors() {
425 assert!(Selector::parse("").is_err());
426 assert!(Selector::parse(" ").is_err());
427 assert!(Selector::parse("a,b").is_err(), "comma not supported");
428 assert!(Selector::parse("[a").is_err(), "unterminated attr");
432 assert!(Selector::parse("[a=").is_err(), "missing value");
433 assert!(Selector::parse("[a='unterminated").is_err());
434 assert!(Selector::parse("#one#two").is_err(), "multiple ids");
435 assert!(Selector::parse("> foo").is_err(), "leading combinator");
436 }
437
438 #[test]
441 fn match_tag() {
442 let s = Selector::parse("item").unwrap();
443 let b = DocumentBuilder::new();
444 assert!( s.matches(make_el(&b, "item", &[]), &[]));
445 assert!(!s.matches(make_el(&b, "entry", &[]), &[]));
446 }
447
448 #[test]
449 fn match_wildcard_any() {
450 let s = Selector::parse("*").unwrap();
451 let b = DocumentBuilder::new();
452 assert!(s.matches(make_el(&b, "anything", &[]), &[]));
453 }
454
455 #[test]
456 fn match_class_token() {
457 let s = Selector::parse(".bar").unwrap();
458 let b = DocumentBuilder::new();
459 assert!( s.matches(make_el(&b, "div", &[("class", "foo bar baz")]), &[]));
460 assert!(!s.matches(make_el(&b, "div", &[("class", "foo baz")]), &[]));
461 }
462
463 #[test]
464 fn match_id_equals() {
465 let s = Selector::parse("#main").unwrap();
466 let b = DocumentBuilder::new();
467 assert!( s.matches(make_el(&b, "div", &[("id", "main")]), &[]));
468 assert!(!s.matches(make_el(&b, "div", &[("id", "main2")]), &[]));
469 }
470
471 #[test]
472 fn match_attr_has_and_equals() {
473 let has = Selector::parse("[hidden]").unwrap();
474 let eq = Selector::parse("[rel=next]").unwrap();
475 let b = DocumentBuilder::new();
476 assert!( has.matches(make_el(&b, "a", &[("hidden", "")]), &[]));
477 assert!(!has.matches(make_el(&b, "a", &[("rel", "next")]), &[]));
478 assert!( eq.matches (make_el(&b, "a", &[("rel", "next")]), &[]));
479 assert!(!eq.matches (make_el(&b, "a", &[("rel", "prev")]), &[]));
480 }
481
482 #[test]
485 fn match_child_combinator() {
486 let s = Selector::parse("channel > item").unwrap();
487 let b = DocumentBuilder::new();
488 let rss = make_el(&b, "rss", &[]);
489 let channel = make_el(&b, "channel", &[]);
490 let item = make_el(&b, "item", &[]);
491 assert!( s.matches(item, &[rss, channel]));
492 assert!(!s.matches(item, &[rss]));
494 }
495
496 #[test]
497 fn match_descendant_combinator() {
498 let s = Selector::parse("rss item").unwrap();
499 let b = DocumentBuilder::new();
500 let rss = make_el(&b, "rss", &[]);
501 let channel = make_el(&b, "channel", &[]);
502 let item = make_el(&b, "item", &[]);
503 assert!(s.matches(item, &[rss, channel]));
504
505 let feed = make_el(&b, "feed", &[]);
506 let channel2 = make_el(&b, "channel", &[]);
507 assert!(!s.matches(item, &[feed, channel2]));
508 }
509
510 #[test]
511 fn match_descendant_backtracks() {
512 let s = Selector::parse("div p").unwrap();
514 let b = DocumentBuilder::new();
515 let outer_div = make_el(&b, "div", &[]);
516 let inner_div = make_el(&b, "div", &[]);
517 let p = make_el(&b, "p", &[]);
518 assert!(s.matches(p, &[outer_div, inner_div]));
519 }
520
521 #[test]
522 fn match_mixed_combinators() {
523 let s = Selector::parse("feed > entry > title").unwrap();
524 let b = DocumentBuilder::new();
525 let feed = make_el(&b, "feed", &[]);
526 let entry = make_el(&b, "entry", &[]);
527 let title = make_el(&b, "title", &[]);
528 assert!( s.matches(title, &[feed, entry]));
529
530 let section = make_el(&b, "section", &[]);
531 assert!(!s.matches(title, &[feed, section]));
532 }
533
534 #[test]
535 fn match_complex_simple_selector_compound() {
536 let s = Selector::parse(r#"entry[type="article"].published"#).unwrap();
537 let b = DocumentBuilder::new();
538 assert!( s.matches(
539 make_el(&b, "entry", &[("type", "article"), ("class", "published")]), &[]));
540 assert!(!s.matches(
541 make_el(&b, "entry", &[("type", "draft"), ("class", "published")]), &[]));
542 assert!(!s.matches(
543 make_el(&b, "entry", &[("type", "article"), ("class", "unpublished")]), &[]));
544 }
545
546 #[test]
547 fn non_element_never_matches() {
548 let s = Selector::parse("*").unwrap();
550 let b = DocumentBuilder::new();
551 let text = b.new_text(b.alloc_str("hello"));
552 assert!(!s.matches(text, &[]));
553 }
554}