Skip to main content

node_html_parser/css_select/
legacy.rs

1use std::collections::HashSet;
2
3use crate::dom::element::HTMLElement;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Combinator {
7	Descendant,
8	Child,
9	Adjacent,
10	Sibling,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum AttrOp {
15	Exists,
16	Eq,
17	Prefix,
18	Suffix,
19	Substr,
20	Includes,
21	Dash,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum CaseMode {
26	Sensitive,
27	Insensitive,
28}
29
30#[derive(Debug, Clone)]
31pub struct AttrMatcher {
32	pub name: String,
33	pub op: AttrOp,
34	pub value: String,
35	pub case: CaseMode,
36}
37
38#[derive(Debug, Clone)]
39pub enum NthExpr {
40	Number(i32),
41	Odd,
42	Even,
43	Pattern { a: i32, b: i32 },
44}
45
46#[derive(Debug, Clone)]
47pub enum Pseudo {
48	FirstChild,
49	LastChild,
50	OnlyChild,
51	FirstOfType,
52	LastOfType,
53	OnlyOfType,
54	NthChild(NthExpr),
55	NthLastChild(NthExpr),
56	NthOfType(NthExpr),
57	NthLastOfType(NthExpr),
58	Not(Vec<Selector>),
59	Scope,
60	Is(Vec<Selector>),
61	Where(Vec<Selector>),
62	Has(Vec<Selector>),
63	Empty,
64	Root,
65}
66
67#[derive(Debug, Clone, Default)]
68pub struct CompoundSelector {
69	pub tag: Option<String>,
70	pub id: Option<String>,
71	pub classes: Vec<String>,
72	pub attrs: Vec<AttrMatcher>,
73	pub pseudos: Vec<Pseudo>,
74}
75
76#[derive(Debug, Clone)]
77pub struct Selector(pub Vec<(Option<Combinator>, CompoundSelector)>);
78
79pub fn query_selector_all<'a>(root: &'a HTMLElement, selector_str: &str) -> Vec<&'a HTMLElement> {
80	let selectors = parse_selector_list(selector_str);
81	if selectors.is_empty() {
82		return vec![];
83	}
84
85	// 注意:由于HTMLElement包含原始指针,不能直接并行处理
86	// 但我们可以并行处理解析后的选择器列表本身
87	// TODO: 未来可以通过重构HTMLElement的内存布局来支持完全并行处理
88
89	let mut ptr_set: HashSet<*const HTMLElement> = HashSet::new();
90	for sel in selectors {
91		for el in apply_selector(root, &sel) {
92			ptr_set.insert(el as *const HTMLElement);
93		}
94	}
95	let mut ordered = Vec::new();
96	collect_in_order_smart(root, &ptr_set, &mut ordered);
97	ordered
98}
99
100fn collect_in_order<'a>(
101	root: &'a HTMLElement,
102	set: &HashSet<*const HTMLElement>,
103	out: &mut Vec<&'a HTMLElement>,
104) {
105	// 优化:使用迭代式遍历替代递归,避免栈溢出并提升性能
106	let mut stack = vec![root];
107
108	while let Some(el) = stack.pop() {
109		if !el.is_root() {
110			let p = el as *const HTMLElement;
111			if set.contains(&p) {
112				out.push(el);
113			}
114		}
115
116		// 收集子元素并逆序入栈以保持文档顺序
117		let children: Vec<_> = el.iter_elements().collect();
118		for child in children.into_iter().rev() {
119			stack.push(child);
120		}
121	}
122}
123
124#[cfg(feature = "parallel")]
125fn collect_in_order_parallel<'a>(
126	root: &'a HTMLElement,
127	set: &HashSet<*const HTMLElement>,
128) -> Vec<&'a HTMLElement> {
129	// 注意:由于HTMLElement包含原始指针,暂时禁用完全并行遍历
130	// 使用改进的迭代版本替代
131	let mut results = Vec::new();
132	collect_in_order(root, set, &mut results);
133	results
134}
135
136// 智能选择遍历策略
137fn collect_in_order_smart<'a>(
138	root: &'a HTMLElement,
139	set: &HashSet<*const HTMLElement>,
140	out: &mut Vec<&'a HTMLElement>,
141) {
142	#[cfg(feature = "parallel")]
143	{
144		// 估算DOM树的宽度和深度来决定使用哪种策略
145		let (width, depth) = estimate_dom_dimensions(root);
146		const PARALLEL_WIDTH_THRESHOLD: usize = 20;
147		const PARALLEL_DEPTH_THRESHOLD: usize = 5;
148
149		// 宽而浅的DOM树适合并行层级遍历
150		if width >= PARALLEL_WIDTH_THRESHOLD && depth <= PARALLEL_DEPTH_THRESHOLD {
151			let parallel_results = collect_in_order_parallel(root, set);
152			out.extend(parallel_results);
153			return;
154		}
155	}
156
157	// 默认使用迭代式遍历
158	collect_in_order(root, set, out);
159}
160
161#[cfg(feature = "parallel")]
162fn estimate_dom_dimensions(root: &HTMLElement) -> (usize, usize) {
163	// 快速估算DOM树的宽度和深度
164	let mut max_width = 0;
165	let mut depth = 0;
166	let mut current_level = vec![root];
167
168	while !current_level.is_empty() && depth < 10 {
169		// 限制深度评估避免过度计算
170		max_width = max_width.max(current_level.len());
171		let next_level: Vec<_> = current_level
172			.iter()
173			.flat_map(|el| el.iter_elements())
174			.collect();
175		current_level = next_level;
176		depth += 1;
177	}
178
179	(max_width, depth)
180}
181
182fn apply_selector<'a>(root: &'a HTMLElement, selector: &Selector) -> Vec<&'a HTMLElement> {
183	let mut current: Vec<&HTMLElement> = vec![root];
184	for (idx, (comb, comp)) in selector.0.iter().enumerate() {
185		let mut next_vec: Vec<&HTMLElement> = Vec::new();
186		for base in &current {
187			let effective = comb.unwrap_or(Combinator::Descendant);
188			match effective {
189				Combinator::Descendant => {
190					if idx == 0 {
191						if match_compound(root, base, comp) {
192							next_vec.push(base);
193						}
194					}
195					collect_descendants(root, base, comp, &mut next_vec);
196				}
197				Combinator::Child => {
198					for child in base.iter_elements() {
199						if match_compound(root, child, comp) {
200							next_vec.push(child);
201						}
202					}
203				}
204				Combinator::Adjacent => {
205					if let Some(parent) = find_parent(root, base) {
206						let sibs: Vec<&HTMLElement> = parent.iter_elements().collect();
207						if let Some(pos) = sibs.iter().position(|e| std::ptr::eq(*e, *base)) {
208							if pos + 1 < sibs.len() {
209								let n = sibs[pos + 1];
210								if match_compound(root, n, comp) {
211									next_vec.push(n);
212								}
213							}
214						}
215					}
216				}
217				Combinator::Sibling => {
218					if let Some(parent) = find_parent(root, base) {
219						let sibs: Vec<&HTMLElement> = parent.iter_elements().collect();
220						if let Some(pos) = sibs.iter().position(|e| std::ptr::eq(*e, *base)) {
221							for n in sibs.iter().skip(pos + 1) {
222								if match_compound(root, *n, comp) {
223									next_vec.push(*n);
224								}
225							}
226						}
227					}
228				}
229			}
230		}
231		let mut seen: HashSet<*const HTMLElement> = HashSet::new();
232		let mut dedup = Vec::new();
233		for e in next_vec {
234			let p = e as *const HTMLElement;
235			if seen.insert(p) {
236				dedup.push(e);
237			}
238		}
239		current = dedup;
240	}
241	current.into_iter().filter(|e| !e.is_root()).collect()
242}
243
244fn collect_descendants<'a>(
245	root: &'a HTMLElement,
246	el: &'a HTMLElement,
247	comp: &CompoundSelector,
248	out: &mut Vec<&'a HTMLElement>,
249) {
250	for child in el.iter_elements() {
251		if match_compound(root, child, comp) {
252			out.push(child);
253		}
254		collect_descendants(root, child, comp, out);
255	}
256}
257
258fn match_compound<'a>(root: &'a HTMLElement, el: &'a HTMLElement, comp: &CompoundSelector) -> bool {
259	if let Some(tag) = &comp.tag {
260		if !el.name().eq_ignore_ascii_case(tag) {
261			return false;
262		}
263	}
264	if let Some(id) = &comp.id {
265		match el.get_attr("id") {
266			Some(v) if v == id => {}
267			_ => return false,
268		}
269	}
270	for class in &comp.classes {
271		if !el.class_list_view().iter().any(|c| c == class) {
272			return false;
273		}
274	}
275	if !comp.attrs.is_empty() {
276		for matcher in &comp.attrs {
277			// Check if element has this attribute (case insensitive name matching)
278			// First ensure all attributes are available for searching
279			let mut_ptr = el as *const HTMLElement as *mut HTMLElement;
280			unsafe {
281				(*mut_ptr).ensure_all_attrs();
282			}
283
284			// Find attribute with case-insensitive name matching
285			let raw_opt = unsafe {
286				(*mut_ptr).attrs.iter().find_map(|(k, v)| {
287					if k.eq_ignore_ascii_case(&matcher.name) {
288						Some(v.as_str())
289					} else {
290						None
291					}
292				})
293			};
294
295			match matcher.op {
296				AttrOp::Exists => {
297					if raw_opt.is_none() {
298						return false;
299					}
300				}
301				_ => {
302					if raw_opt.is_none() {
303						return false;
304					}
305					let val = raw_opt.unwrap();
306					let (left, right) = match matcher.case {
307						CaseMode::Insensitive => {
308							(val.to_ascii_lowercase(), matcher.value.to_ascii_lowercase())
309						}
310						CaseMode::Sensitive => (val.to_string(), matcher.value.clone()),
311					};
312					let ok = match matcher.op {
313						AttrOp::Exists => true,
314						AttrOp::Eq => left == right,
315						AttrOp::Prefix => left.starts_with(&right),
316						AttrOp::Suffix => left.ends_with(&right),
317						AttrOp::Substr => left.contains(&right),
318						AttrOp::Includes => left.split_whitespace().any(|t| t == right),
319						AttrOp::Dash => left == right || left.starts_with(&(right + "-")),
320					};
321					if !ok {
322						return false;
323					}
324				}
325			}
326		}
327	}
328	if !comp.pseudos.is_empty() {
329		for p in &comp.pseudos {
330			if !match_pseudo(root, el, p) {
331				return false;
332			}
333		}
334	}
335	true
336}
337
338fn match_pseudo<'a>(root: &'a HTMLElement, el: &'a HTMLElement, pseudo: &Pseudo) -> bool {
339	match pseudo {
340		Pseudo::FirstChild => position_in_parent(root, el)
341			.map(|(i, _, _, _)| i == 0)
342			.unwrap_or(false),
343		Pseudo::LastChild => position_in_parent(root, el)
344			.map(|(i, len, _, _)| i + 1 == len)
345			.unwrap_or(false),
346		Pseudo::OnlyChild => position_in_parent(root, el)
347			.map(|(_, len, _, _)| len == 1)
348			.unwrap_or(false),
349		Pseudo::FirstOfType => position_in_parent(root, el)
350			.map(|(_, _, ti, _)| ti == 0)
351			.unwrap_or(false),
352		Pseudo::LastOfType => position_in_parent(root, el)
353			.map(|(_, _, ti, tlen)| ti + 1 == tlen)
354			.unwrap_or(false),
355		Pseudo::OnlyOfType => position_in_parent(root, el)
356			.map(|(_, _, _, tlen)| tlen == 1)
357			.unwrap_or(false),
358		Pseudo::NthChild(expr) => position_in_parent(root, el)
359			.map(|(i, _, _, _)| match_nth(expr, i as i32 + 1))
360			.unwrap_or(false),
361		Pseudo::NthLastChild(expr) => position_in_parent(root, el)
362			.map(|(i, len, _, _)| {
363				let rev = (len - i - 1) as i32 + 1;
364				match_nth(expr, rev)
365			})
366			.unwrap_or(false),
367		Pseudo::NthOfType(expr) => position_in_parent(root, el)
368			.map(|(_, _, ti, _)| match_nth(expr, ti as i32 + 1))
369			.unwrap_or(false),
370		Pseudo::NthLastOfType(expr) => position_in_parent(root, el)
371			.map(|(_, _, ti, tlen)| {
372				let rev = (tlen - ti - 1) as i32 + 1;
373				match_nth(expr, rev)
374			})
375			.unwrap_or(false),
376		Pseudo::Not(list) => !list.iter().any(|sel| apply_selector_from_el(root, el, sel)),
377		Pseudo::Empty => {
378			el.iter_elements().next().is_none()
379				&& el
380					.children
381					.iter()
382					.all(|n| n.as_element().is_some() || n.text().trim().is_empty())
383		}
384		Pseudo::Root => find_parent(root, el).map(|p| p.is_root()).unwrap_or(false),
385		Pseudo::Is(list) => list.iter().any(|sel| apply_selector_from_el(root, el, sel)),
386		Pseudo::Where(list) => list.iter().any(|sel| apply_selector_from_el(root, el, sel)),
387		Pseudo::Has(list) => {
388			// :has(A) 若当前元素存在后代匹配 A;使用 DFS
389			fn any_desc<'a>(
390				cur: &'a HTMLElement,
391				root: &'a HTMLElement,
392				list: &Vec<Selector>,
393			) -> bool {
394				for child in cur.iter_elements() {
395					for sel in list {
396						if apply_selector_from_el(root, child, sel) {
397							return true;
398						}
399					}
400					if any_desc(child, root, list) {
401						return true;
402					}
403				}
404				false
405			}
406			any_desc(el, root, list)
407		}
408		Pseudo::Scope => true,
409	}
410}
411
412fn match_nth(expr: &NthExpr, index_one: i32) -> bool {
413	match expr {
414		NthExpr::Number(n) => index_one == *n,
415		NthExpr::Odd => (index_one % 2) == 1,
416		NthExpr::Even => (index_one % 2) == 0,
417		NthExpr::Pattern { a, b } => {
418			let a = *a;
419			let b = *b;
420			if a == 0 {
421				return index_one == b;
422			}
423			if a > 0 {
424				if index_one < b {
425					return false;
426				}
427				(index_one - b) % a == 0
428			} else {
429				let mut k = 0;
430				loop {
431					let val = a * k + b;
432					if val == index_one {
433						return true;
434					}
435					if val < 1 {
436						return false;
437					}
438					if val < index_one {
439						k += 1;
440						continue;
441					}
442					return false;
443				}
444			}
445		}
446	}
447}
448
449fn position_in_parent<'a>(
450	root: &'a HTMLElement,
451	el: &'a HTMLElement,
452) -> Option<(usize, usize, usize, usize)> {
453	let parent = find_parent(root, el)?;
454	let list: Vec<&HTMLElement> = parent.iter_elements().collect();
455	if list.is_empty() {
456		return None;
457	}
458	let same: Vec<&HTMLElement> = list
459		.iter()
460		.copied()
461		.filter(|c| c.name() == el.name())
462		.collect();
463	let idx = list.iter().position(|e| std::ptr::eq(*e, el))?;
464	let tidx = same.iter().position(|e| std::ptr::eq(*e, el))?;
465	Some((idx, list.len(), tidx, same.len()))
466}
467
468fn find_parent<'a>(root: &'a HTMLElement, target: &'a HTMLElement) -> Option<&'a HTMLElement> {
469	if std::ptr::eq(root, target) {
470		return None;
471	}
472	fn dfs<'a>(cur: &'a HTMLElement, target: &'a HTMLElement) -> Option<&'a HTMLElement> {
473		for child in cur.iter_elements() {
474			if std::ptr::eq(child, target) {
475				return Some(cur);
476			}
477			if let Some(p) = dfs(child, target) {
478				return Some(p);
479			}
480		}
481		None
482	}
483	dfs(root, target)
484}
485
486fn parse_selector_list(input: &str) -> Vec<Selector> {
487	let mut parts = Vec::new();
488	let mut buf = String::new();
489	let mut depth = 0; // parentheses depth
490	for c in input.chars() {
491		match c {
492			'(' => {
493				depth += 1;
494				buf.push(c);
495			}
496			')' => {
497				if depth > 0 {
498					depth -= 1;
499				}
500				buf.push(c);
501			}
502			',' if depth == 0 => {
503				let t = buf.trim();
504				if !t.is_empty() {
505					parts.push(t.to_string());
506				}
507				buf.clear();
508			}
509			_ => buf.push(c),
510		}
511	}
512	let t = buf.trim();
513	if !t.is_empty() {
514		parts.push(t.to_string());
515	}
516	parts
517		.into_iter()
518		.filter_map(|p| {
519			let tt = p.trim();
520			if tt.is_empty() {
521				None
522			} else {
523				Some(parse_single_selector(tt))
524			}
525		})
526		.collect()
527}
528
529fn parse_single_selector(input: &str) -> Selector {
530	let mut chars = input.chars().peekable();
531	let mut parts: Vec<(Option<Combinator>, CompoundSelector)> = Vec::new();
532	let mut current = CompoundSelector::default();
533	let mut pending: Option<Combinator> = None;
534	while let Some(&ch) = chars.peek() {
535		match ch {
536			' ' | '\t' | '\n' | '\r' => {
537				chars.next();
538				if !current_is_empty(&current) {
539					parts.push((pending.take(), current));
540					current = CompoundSelector::default();
541				}
542				pending.get_or_insert(Combinator::Descendant);
543				while let Some(&c2) = chars.peek() {
544					if c2.is_whitespace() {
545						chars.next();
546					} else {
547						break;
548					}
549				}
550			}
551			'>' => {
552				chars.next();
553				if !current_is_empty(&current) {
554					parts.push((pending.take(), current));
555					current = CompoundSelector::default();
556				}
557				pending = Some(Combinator::Child);
558				skip_ws(&mut chars);
559			}
560			'+' => {
561				chars.next();
562				if !current_is_empty(&current) {
563					parts.push((pending.take(), current));
564					current = CompoundSelector::default();
565				}
566				pending = Some(Combinator::Adjacent);
567				skip_ws(&mut chars);
568			}
569			'~' => {
570				chars.next();
571				if !current_is_empty(&current) {
572					parts.push((pending.take(), current));
573					current = CompoundSelector::default();
574				}
575				pending = Some(Combinator::Sibling);
576				skip_ws(&mut chars);
577			}
578			'#' => {
579				chars.next();
580				let ident = read_ident(&mut chars);
581				current.id = Some(ident);
582			}
583			'.' => {
584				chars.next();
585				let ident = read_ident(&mut chars);
586				current.classes.push(ident);
587			}
588			'[' => {
589				chars.next();
590				let name = read_ident(&mut chars);
591				skip_ws(&mut chars);
592				let mut op = AttrOp::Exists;
593				let mut value = String::new();
594				let mut case = CaseMode::Sensitive;
595				if let Some(&c2) = chars.peek() {
596					match c2 {
597						'=' => {
598							op = AttrOp::Eq;
599							chars.next();
600							skip_ws(&mut chars);
601							value = read_attr_value(&mut chars);
602						}
603						'^' | '$' | '*' | '~' | '|' => {
604							let first = c2;
605							chars.next();
606							if matches!(chars.peek(), Some('=')) {
607								chars.next();
608								op = match first {
609									'^' => AttrOp::Prefix,
610									'$' => AttrOp::Suffix,
611									'*' => AttrOp::Substr,
612									'~' => AttrOp::Includes,
613									'|' => AttrOp::Dash,
614									_ => AttrOp::Eq,
615								};
616								skip_ws(&mut chars);
617								value = read_attr_value(&mut chars);
618							}
619						}
620						_ => {}
621					}
622				}
623				skip_ws(&mut chars);
624				if let Some(&flag) = chars.peek() {
625					if flag == 'i' {
626						case = CaseMode::Insensitive;
627						chars.next();
628					} else if flag == 's' {
629						case = CaseMode::Sensitive;
630						chars.next();
631					}
632				}
633				while let Some(c) = chars.next() {
634					if c == ']' {
635						break;
636					}
637				}
638				current.attrs.push(AttrMatcher {
639					name: name.to_lowercase(),
640					op,
641					value,
642					case,
643				});
644			}
645			'*' => {
646				chars.next();
647			}
648			':' => {
649				chars.next();
650				let pseudo_name = read_ident(&mut chars).to_ascii_lowercase();
651				let pseudo = if matches!(chars.peek(), Some('(')) {
652					chars.next();
653					let arg = read_until_paren(&mut chars);
654					parse_pseudo_with_arg(&pseudo_name, &arg)
655				} else {
656					parse_pseudo_no_arg(&pseudo_name)
657				};
658				if let Some(p) = pseudo {
659					current.pseudos.push(p);
660				}
661			}
662			_ => {
663				if current.tag.is_none() {
664					let ident = read_ident_starting(ch, &mut chars);
665					current.tag = Some(ident.to_lowercase());
666				} else {
667					chars.next();
668				}
669			}
670		}
671	}
672	if !current_is_empty(&current) || parts.is_empty() {
673		parts.push((pending.take(), current));
674	}
675	Selector(parts)
676}
677
678fn current_is_empty(c: &CompoundSelector) -> bool {
679	c.tag.is_none()
680		&& c.id.is_none()
681		&& c.classes.is_empty()
682		&& c.attrs.is_empty()
683		&& c.pseudos.is_empty()
684}
685fn skip_ws<I: Iterator<Item = char>>(it: &mut std::iter::Peekable<I>) {
686	while matches!(it.peek(), Some(c) if c.is_whitespace()) {
687		it.next();
688	}
689}
690fn read_ident<I: Iterator<Item = char>>(it: &mut std::iter::Peekable<I>) -> String {
691	let mut s = String::new();
692	while let Some(&c) = it.peek() {
693		if c.is_alphanumeric() || c == '-' || c == '_' {
694			s.push(c);
695			it.next();
696		} else {
697			break;
698		}
699	}
700	s
701}
702fn read_ident_starting(first: char, it: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
703	let mut s = String::new();
704	s.push(first);
705	it.next();
706	while let Some(&c) = it.peek() {
707		if c.is_alphanumeric() || c == '-' || c == '_' {
708			s.push(c);
709			it.next();
710		} else {
711			break;
712		}
713	}
714	s
715}
716fn read_attr_value<I: Iterator<Item = char>>(it: &mut std::iter::Peekable<I>) -> String {
717	if let Some(&q) = it.peek() {
718		if q == '"' || q == '\'' {
719			it.next();
720			let mut val = String::new();
721			while let Some(&c) = it.peek() {
722				it.next();
723				if c == q {
724					break;
725				}
726				val.push(c);
727			}
728			return val;
729		}
730	}
731	read_ident(it)
732}
733fn read_until_paren<I: Iterator<Item = char>>(it: &mut std::iter::Peekable<I>) -> String {
734	let mut depth = 1;
735	let mut s = String::new();
736	while let Some(&c) = it.peek() {
737		it.next();
738		if c == '(' {
739			depth += 1;
740		} else if c == ')' {
741			depth -= 1;
742			if depth == 0 {
743				break;
744			}
745		}
746		s.push(c);
747	}
748	s.trim().to_string()
749}
750fn parse_pseudo_no_arg(name: &str) -> Option<Pseudo> {
751	Some(match name {
752		"first-child" => Pseudo::FirstChild,
753		"last-child" => Pseudo::LastChild,
754		"only-child" => Pseudo::OnlyChild,
755		"first-of-type" => Pseudo::FirstOfType,
756		"last-of-type" => Pseudo::LastOfType,
757		"only-of-type" => Pseudo::OnlyOfType,
758		"empty" => Pseudo::Empty,
759		"root" => Pseudo::Root,
760		"scope" => Pseudo::Scope,
761		_ => return None,
762	})
763}
764fn parse_pseudo_with_arg(name: &str, arg: &str) -> Option<Pseudo> {
765	match name {
766		"nth-child" => Some(Pseudo::NthChild(parse_nth_expr(arg)?)),
767		"nth-last-child" => Some(Pseudo::NthLastChild(parse_nth_expr(arg)?)),
768		"nth-of-type" => Some(Pseudo::NthOfType(parse_nth_expr(arg)?)),
769		"nth-last-of-type" => Some(Pseudo::NthLastOfType(parse_nth_expr(arg)?)),
770		"not" => {
771			let sels = parse_selector_list(arg);
772			if sels.is_empty() {
773				None
774			} else {
775				Some(Pseudo::Not(sels))
776			}
777		}
778		"is" => {
779			let sels = parse_selector_list(arg);
780			if sels.is_empty() {
781				None
782			} else {
783				Some(Pseudo::Is(sels))
784			}
785		}
786		"where" => {
787			let sels = parse_selector_list(arg);
788			if sels.is_empty() {
789				None
790			} else {
791				Some(Pseudo::Where(sels))
792			}
793		}
794		"has" => {
795			let sels = parse_selector_list(arg);
796			if sels.is_empty() {
797				None
798			} else {
799				Some(Pseudo::Has(sels))
800			}
801		}
802		_ => None,
803	}
804}
805fn parse_nth_expr(s: &str) -> Option<NthExpr> {
806	let t = s.trim().to_ascii_lowercase();
807	if t == "odd" {
808		return Some(NthExpr::Odd);
809	}
810	if t == "even" {
811		return Some(NthExpr::Even);
812	}
813	if let Ok(n) = t.parse::<i32>() {
814		if n > 0 {
815			return Some(NthExpr::Number(n));
816		}
817	}
818	if let Some(pos) = t.find('n') {
819		let (a_part, rest) = t.split_at(pos);
820		let rest = &rest[1..];
821		let a = if a_part.is_empty() || a_part == "+" {
822			1
823		} else if a_part == "-" {
824			-1
825		} else {
826			a_part.parse().ok()?
827		};
828		let b = if rest.is_empty() {
829			0
830		} else {
831			let r = rest.trim();
832			if r.starts_with('+') {
833				r[1..].parse().ok()?
834			} else {
835				r.parse().ok()?
836			}
837		};
838		return Some(NthExpr::Pattern { a, b });
839	}
840	None
841}
842
843pub fn apply_selector_from_el<'a>(
844	root: &'a HTMLElement,
845	el: &'a HTMLElement,
846	selector: &Selector,
847) -> bool {
848	let matches = apply_selector(root, selector);
849	matches.into_iter().any(|e| std::ptr::eq(e, el))
850}
851pub fn parse_selector_list_public(input: &str) -> Vec<Selector> {
852	parse_selector_list(input)
853}
854
855// Expose helper for conversion: return reference parts
856pub fn selector_parts(sel: &Selector) -> &Vec<(Option<Combinator>, CompoundSelector)> {
857	&sel.0
858}