use cssparser::{CowRcStr, ToCss}; use html5ever::{LocalName, Namespace}; use precomputed_hash::PrecomputedHash; use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; use selectors::bloom::BloomFilter; use selectors::context::QuirksMode; use selectors::matching::{ ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, }; use selectors::parser::{self, ParseRelative, SelectorParseErrorKind}; use selectors::{Element, OpaqueElement, SelectorList}; use selectors::visitor::SelectorVisitor; use crate::tree::{DomTree, NodeData, NodeId}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ObscuraSelector; impl parser::SelectorImpl for ObscuraSelector { type ExtraMatchingData<'a> = (); type AttrValue = CssString; type Identifier = CssString; type LocalName = CssLocalName; type NamespaceUrl = CssNamespace; type NamespacePrefix = CssString; type BorrowedLocalName = CssLocalName; type BorrowedNamespaceUrl = CssNamespace; type NonTSPseudoClass = PseudoClass; type PseudoElement = PseudoElement; } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CssString(pub String); impl<'a> From<&'a str> for CssString { fn from(s: &'a str) -> Self { CssString(s.to_string()) } } impl AsRef for CssString { fn as_ref(&self) -> &str { &self.0 } } impl ToCss for CssString { fn to_css(&self, dest: &mut W) -> std::fmt::Result { cssparser::serialize_string(&self.0, dest) } } impl PrecomputedHash for CssString { fn precomputed_hash(&self) -> u32 { let mut h: u32 = 5381; for b in self.0.as_bytes() { h = h.wrapping_mul(33).wrapping_add(*b as u32); } h } } impl Default for CssString { fn default() -> Self { CssString(String::new()) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CssLocalName(pub LocalName); impl<'a> From<&'a str> for CssLocalName { fn from(s: &'a str) -> Self { CssLocalName(LocalName::from(s)) } } impl ToCss for CssLocalName { fn to_css(&self, dest: &mut W) -> std::fmt::Result { dest.write_str(&self.0) } } impl PrecomputedHash for CssLocalName { fn precomputed_hash(&self) -> u32 { self.0.precomputed_hash() } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct CssNamespace(pub Namespace); impl PrecomputedHash for CssNamespace { fn precomputed_hash(&self) -> u32 { self.0.precomputed_hash() } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum PseudoClass { Hover, Active, Focus, Enabled, Disabled, Checked, } impl parser::NonTSPseudoClass for PseudoClass { type Impl = ObscuraSelector; fn is_active_or_hover(&self) -> bool { matches!(self, PseudoClass::Hover | PseudoClass::Active) } fn is_user_action_state(&self) -> bool { matches!( self, PseudoClass::Hover | PseudoClass::Active | PseudoClass::Focus ) } fn visit(&self, _visitor: &mut V) -> bool where V: SelectorVisitor, { true } } impl ToCss for PseudoClass { fn to_css(&self, dest: &mut W) -> std::fmt::Result { match self { PseudoClass::Hover => dest.write_str(":hover"), PseudoClass::Active => dest.write_str(":active"), PseudoClass::Focus => dest.write_str(":focus"), PseudoClass::Enabled => dest.write_str(":enabled"), PseudoClass::Disabled => dest.write_str(":disabled"), PseudoClass::Checked => dest.write_str(":checked"), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum PseudoElement { Before, After, } impl parser::PseudoElement for PseudoElement { type Impl = ObscuraSelector; } impl ToCss for PseudoElement { fn to_css(&self, dest: &mut W) -> std::fmt::Result { match self { PseudoElement::Before => dest.write_str("::before"), PseudoElement::After => dest.write_str("::after"), } } } pub struct ObscuraSelectorParser; impl<'i> parser::Parser<'i> for ObscuraSelectorParser { type Impl = ObscuraSelector; type Error = SelectorParseErrorKind<'i>; // Allow `:has()`. The selectors crate gates relative-selector parsing on // this (default false), so without it `a:has(p)` failed to parse and the // error was swallowed into an empty match set. Matching already passes a // SelectorCaches (which holds the relative-selector cache), so enabling // parsing is sufficient. fn parse_has(&self) -> bool { true } fn parse_non_ts_pseudo_class( &self, _location: cssparser::SourceLocation, name: CowRcStr<'i>, ) -> Result> { match name.as_ref() { "hover" => Ok(PseudoClass::Hover), "active" => Ok(PseudoClass::Active), "focus" => Ok(PseudoClass::Focus), "enabled" => Ok(PseudoClass::Enabled), "disabled" => Ok(PseudoClass::Disabled), "checked" => Ok(PseudoClass::Checked), _ => Err(cssparser::ParseError { kind: cssparser::ParseErrorKind::Custom( SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name), ), location: _location, }), } } } #[derive(Clone, Copy)] pub struct DomElement<'a> { pub tree: &'a DomTree, pub node_id: NodeId, } impl<'a> DomElement<'a> { pub fn new(tree: &'a DomTree, node_id: NodeId) -> Self { DomElement { tree, node_id } } } impl<'a> std::fmt::Debug for DomElement<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "DomElement({:?})", self.node_id) } } impl<'a> PartialEq for DomElement<'a> { fn eq(&self, other: &Self) -> bool { self.node_id == other.node_id } } impl<'a> Eq for DomElement<'a> {} impl<'a> Element for DomElement<'a> { type Impl = ObscuraSelector; fn opaque(&self) -> OpaqueElement { // Must be stable per node. DomElement is Copy and gets a fresh stack // address on every traversal step, so OpaqueElement::new(self) returns a // different id for the same node each call. That breaks `:has` anchor // matching, which compares the anchor's opaque against the element // reached by walking up the tree. Key off the node's stable slot in the // tree's Vec instead (OpaqueElement only compares the address, never // dereferences it, and the Vec is not mutated during a query). let inner = self.tree.borrow_inner(); match inner.nodes.get(self.node_id.index()) { Some(slot) => OpaqueElement::new(slot), None => OpaqueElement::new(self), } } fn parent_element(&self) -> Option { let node = self.tree.get_node(self.node_id)?; let parent_id = node.parent?; let parent = self.tree.get_node(parent_id)?; if parent.is_element() { Some(DomElement::new(self.tree, parent_id)) } else { None } } fn parent_node_is_shadow_root(&self) -> bool { false } fn containing_shadow_host(&self) -> Option { None } fn pseudo_element_originating_element(&self) -> Option { None } fn is_pseudo_element(&self) -> bool { false } fn prev_sibling_element(&self) -> Option { let node = self.tree.get_node(self.node_id)?; let mut current = node.prev_sibling; while let Some(sibling_id) = current { let sibling = self.tree.get_node(sibling_id)?; if sibling.is_element() { return Some(DomElement::new(self.tree, sibling_id)); } current = sibling.prev_sibling; } None } fn next_sibling_element(&self) -> Option { let node = self.tree.get_node(self.node_id)?; let mut current = node.next_sibling; while let Some(sibling_id) = current { let sibling = self.tree.get_node(sibling_id)?; if sibling.is_element() { return Some(DomElement::new(self.tree, sibling_id)); } current = sibling.next_sibling; } None } fn first_element_child(&self) -> Option { let node = self.tree.get_node(self.node_id)?; let mut current = node.first_child; while let Some(child_id) = current { let child = self.tree.get_node(child_id)?; if child.is_element() { return Some(DomElement::new(self.tree, child_id)); } current = child.next_sibling; } None } fn is_html_element_in_html_document(&self) -> bool { self.tree .with_node(self.node_id, |n| { n.as_element() .map(|name| name.ns == ns!(html)) .unwrap_or(false) }) .unwrap_or(false) } fn has_local_name(&self, local_name: &CssLocalName) -> bool { self.tree .with_node(self.node_id, |n| { n.as_element() .map(|name| name.local == local_name.0) .unwrap_or(false) }) .unwrap_or(false) } fn has_namespace(&self, ns: &CssNamespace) -> bool { self.tree .with_node(self.node_id, |n| { n.as_element() .map(|name| name.ns == ns.0) .unwrap_or(false) }) .unwrap_or(false) } fn is_same_type(&self, other: &Self) -> bool { let self_name = self.tree.with_node(self.node_id, |n| { n.as_element().map(|name| (name.local.clone(), name.ns.clone())) }).flatten(); let other_name = self.tree.with_node(other.node_id, |n| { n.as_element().map(|name| (name.local.clone(), name.ns.clone())) }).flatten(); match (self_name, other_name) { (Some((al, ans)), Some((bl, bns))) => al == bl && ans == bns, _ => false, } } fn attr_matches( &self, ns: &NamespaceConstraint<&CssNamespace>, local_name: &CssLocalName, operation: &AttrSelectorOperation<&CssString>, ) -> bool { self.tree .with_node(self.node_id, |node| { let attrs = match node.attrs() { Some(a) => a, None => return false, }; attrs.iter().any(|attr| { let ns_match = match ns { NamespaceConstraint::Any => true, NamespaceConstraint::Specific(expected_ns) => attr.name.ns == expected_ns.0, }; if !ns_match || attr.name.local != local_name.0 { return false; } operation.eval_str(&attr.value) }) }) .unwrap_or(false) } fn has_attr_in_no_namespace(&self, local_name: &CssLocalName) -> bool { self.tree .with_node(self.node_id, |node| { node.attrs() .map(|attrs| { attrs.iter().any(|a| { a.name.ns == html5ever::ns!() && a.name.local == local_name.0 }) }) .unwrap_or(false) }) .unwrap_or(false) } fn match_non_ts_pseudo_class( &self, _pc: &PseudoClass, _context: &mut MatchingContext<'_, Self::Impl>, ) -> bool { false } fn match_pseudo_element( &self, _pe: &PseudoElement, _context: &mut MatchingContext<'_, Self::Impl>, ) -> bool { false } fn apply_selector_flags(&self, _flags: ElementSelectorFlags) {} fn is_link(&self) -> bool { self.tree .with_node(self.node_id, |n| { n.as_element() .map(|name| { matches!(name.local.as_ref(), "a" | "area" | "link") && n.get_attribute("href").is_some() }) .unwrap_or(false) }) .unwrap_or(false) } fn is_html_slot_element(&self) -> bool { false } fn assigned_slot(&self) -> Option { None } fn has_id(&self, id: &CssString, case_sensitivity: CaseSensitivity) -> bool { self.tree .with_node(self.node_id, |n| { n.get_attribute("id") .map(|value| case_sensitivity.eq(value.as_bytes(), id.0.as_bytes())) .unwrap_or(false) }) .unwrap_or(false) } fn has_class(&self, name: &CssString, case_sensitivity: CaseSensitivity) -> bool { self.tree .with_node(self.node_id, |n| { n.get_attribute("class") .map(|class_attr| { class_attr .split_whitespace() .any(|c| case_sensitivity.eq(c.as_bytes(), name.0.as_bytes())) }) .unwrap_or(false) }) .unwrap_or(false) } fn has_custom_state(&self, _name: &CssString) -> bool { false } fn imported_part(&self, _name: &CssString) -> Option { None } fn is_part(&self, _name: &CssString) -> bool { false } fn is_empty(&self) -> bool { self.tree .with_node(self.node_id, |node| { let mut child = node.first_child; while let Some(child_id) = child { if let Some(child_node) = self.tree.get_node(child_id) { match &child_node.data { NodeData::Element { .. } => return false, NodeData::Text { contents } if !contents.is_empty() => return false, _ => {} } child = child_node.next_sibling; } else { break; } } true }) .unwrap_or(true) } fn is_root(&self) -> bool { self.tree .with_node(self.node_id, |n| { n.parent .map(|parent_id| { self.tree .with_node(parent_id, |p| p.is_document()) .unwrap_or(false) }) .unwrap_or(false) }) .unwrap_or(false) } fn ignores_nth_child_selectors(&self) -> bool { false } fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool { false } } // Thread-local LRU cache of parsed selectors. Without this every // querySelector / querySelectorAll re-parses the selector string; // for batch-heavy DOM access (agent scraping a table, framework // repeatedly polling for elements) the parse cost adds up to tens // of ms per page. Cap of 256 entries fits a typical page's distinct // selectors without unbounded memory growth. thread_local! { static SELECTOR_CACHE: std::cell::RefCell< std::collections::HashMap>>, > = std::cell::RefCell::new(std::collections::HashMap::with_capacity(64)); } const SELECTOR_CACHE_CAP: usize = 256; fn parse_selector_uncached(selector: &str) -> Result, String> { let mut parser_input = cssparser::ParserInput::new(selector); let mut parser = cssparser::Parser::new(&mut parser_input); SelectorList::parse(&ObscuraSelectorParser, &mut parser, ParseRelative::No) .map_err(|e| format!("Failed to parse selector '{}': {:?}", selector, e)) } pub fn parse_selector(selector: &str) -> Result, String> { // Hot path: cached. Cold path: parse + insert. if let Some(cached) = SELECTOR_CACHE.with(|c| c.borrow().get(selector).cloned()) { return Ok((*cached).clone()); } let parsed = parse_selector_uncached(selector)?; let cached = std::sync::Arc::new(parsed.clone()); SELECTOR_CACHE.with(|c| { let mut cache = c.borrow_mut(); // Crude eviction: if at cap, dump the whole table. A real LRU // would be more memory-friendly but selectors are small and 256 // is comfortably above a single page's distinct-selector count. if cache.len() >= SELECTOR_CACHE_CAP { cache.clear(); } cache.insert(selector.to_string(), cached); }); Ok(parsed) } /// If `selector` is a bare ASCII id selector like `#main`, return the id (without /// the `#`). Conservative: escapes, combinators, commas, whitespace, non-ASCII, or /// a non-letter/underscore first character fall through to the full selector engine. fn simple_id_selector(selector: &str) -> Option<&str> { let id = selector.trim().strip_prefix('#')?; let first = id.as_bytes().first().copied()?; if !(first.is_ascii_alphabetic() || first == b'_') { return None; } if id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') { Some(id) } else { None } } impl DomTree { pub fn query_selector(&self, selector: &str) -> Result, String> { self.query_selector_from(self.document(), selector) } pub fn query_selector_all(&self, selector: &str) -> Result, String> { self.query_selector_all_from(self.document(), selector) } pub fn query_selector_from(&self, root: NodeId, selector: &str) -> Result, String> { // Fast path: a bare "#id" selector resolves through the id index in O(1) // instead of scanning every descendant. The index holds the first element // in tree order per id, which is exactly what the full scan would return. // In quirks mode `#id` matches ASCII-case-insensitively, but the id // index is keyed on the exact-case id, so skip the fast path and let the // selector engine (below) do the case-insensitive match. if !self.is_quirks() { if let Some(id) = simple_id_selector(selector) { match self.get_element_by_id(id) { // querySelector matches strict descendants of root only, so the // indexed element must have root among its ancestors. Some(nid) if self.ancestors(nid).contains(&root) => return Ok(Some(nid)), // Index miss or stale entry (detached node) — fall through to full scan. // The id_index is best-effort: it only registers nodes at creation time // and does not update on reparent, so it can point to a detached clone // while the live node (with the same id) is elsewhere in the tree. _ => {} } } } let selector_list = parse_selector(selector)?; let mut caches = selectors::context::SelectorCaches::default(); let mut context = MatchingContext::new( MatchingMode::Normal, None, &mut caches, self.selector_quirks_mode(), NeedsSelectorFlags::No, MatchingForInvalidation::No, ); for desc_id in self.descendants(root) { let is_element = self.with_node(desc_id, |n| n.is_element()).unwrap_or(false); if is_element { let element = DomElement::new(self, desc_id); if selectors::matching::matches_selector_list( &selector_list, &element, &mut context, ) { return Ok(Some(desc_id)); } } } Ok(None) } // Map the document's quirks flag onto the selector crate's QuirksMode. In // quirks mode the crate matches class/id selectors ASCII-case-insensitively. fn selector_quirks_mode(&self) -> QuirksMode { if self.is_quirks() { QuirksMode::Quirks } else { QuirksMode::NoQuirks } } pub fn query_selector_all_from(&self, root: NodeId, selector: &str) -> Result, String> { let selector_list = parse_selector(selector)?; let mut caches = selectors::context::SelectorCaches::default(); let mut context = MatchingContext::new( MatchingMode::Normal, None, &mut caches, self.selector_quirks_mode(), NeedsSelectorFlags::No, MatchingForInvalidation::No, ); let mut results = Vec::new(); for desc_id in self.descendants(root) { let is_element = self.with_node(desc_id, |n| n.is_element()).unwrap_or(false); if is_element { let element = DomElement::new(self, desc_id); if selectors::matching::matches_selector_list( &selector_list, &element, &mut context, ) { results.push(desc_id); } } } Ok(results) } } #[cfg(test)] mod tests { use crate::tree_sink::parse_html; #[test] fn test_query_selector_tag() { let tree = parse_html("

Title

Text

"); let result = tree.query_selector("h1").unwrap(); assert!(result.is_some()); let node = tree.get_node(result.unwrap()).unwrap(); assert_eq!(node.as_element().unwrap().local.as_ref(), "h1"); } #[test] fn test_query_selector_class() { let tree = parse_html(r#"
Content
Other
"#); let result = tree.query_selector(".foo").unwrap(); assert!(result.is_some()); let node = tree.get_node(result.unwrap()).unwrap(); assert_eq!(node.get_attribute("class"), Some("foo bar")); } #[test] fn test_query_selector_id() { let tree = parse_html(r#"
Content
"#); let result = tree.query_selector("#main").unwrap(); assert!(result.is_some()); } #[test] fn test_query_selector_all() { let tree = parse_html("
  • 1
  • 2
  • 3
"); let results = tree.query_selector_all("li").unwrap(); assert_eq!(results.len(), 3); } #[test] fn test_has_parses() { // Isolate parse from match: :has must parse, not error. assert!(super::parse_selector("a:has(p.bt)").is_ok(), ":has failed to parse"); } #[test] fn test_query_selector_has() { let tree = parse_html(r#"

x

"#); let all = tree.query_selector_all("a:has(p.bt)").unwrap(); assert_eq!(all.len(), 1, "a:has(p.bt) should match the "); let none = tree.query_selector_all("a:has(span.bt)").unwrap(); assert_eq!(none.len(), 0, "a:has(span.bt) should match nothing"); } #[test] fn test_query_selector_descendant() { let tree = parse_html(r#"
Target
"#); let result = tree.query_selector("#outer span").unwrap(); assert!(result.is_some()); let node = tree.get_node(result.unwrap()).unwrap(); assert_eq!(node.as_element().unwrap().local.as_ref(), "span"); } #[test] fn test_query_selector_attribute() { let tree = parse_html( r#""#, ); let result = tree.query_selector(r#"input[type="password"]"#).unwrap(); assert!(result.is_some()); let node = tree.get_node(result.unwrap()).unwrap(); assert_eq!(node.get_attribute("name"), Some("pass")); } #[test] fn test_query_selector_no_match() { let tree = parse_html("
Hello
"); let result = tree.query_selector("span").unwrap(); assert!(result.is_none()); } #[test] fn quirks_mode_matches_class_and_id_case_insensitively() { // No doctype => quirks mode; class/id match ASCII case-insensitively. let tree = parse_html(r#"
x
"#); assert!( tree.query_selector(".foo").unwrap().is_some(), ".foo should match class=\"Foo\" in quirks mode" ); assert!( tree.query_selector("#bar").unwrap().is_some(), "#bar should match id=\"Bar\" in quirks mode" ); } #[test] fn standards_mode_matches_class_and_id_case_sensitively() { // With a doctype => no-quirks; class/id remain case-sensitive. let tree = parse_html(r#"
x
"#); assert!( tree.query_selector(".foo").unwrap().is_none(), ".foo must NOT match class=\"Foo\" in standards mode" ); assert!( tree.query_selector("#bar").unwrap().is_none(), "#bar must NOT match id=\"Bar\" in standards mode" ); } #[test] fn test_query_selector_complex() { let tree = parse_html( r#"
  • First
  • Second
  • Third
"#, ); let results = tree.query_selector_all(".list .item.active").unwrap(); assert_eq!(results.len(), 2); } #[test] fn test_query_selector_all_from_scopes_to_subtree() { let tree = parse_html( r#"
in a
in b
"#, ); let a = tree.get_element_by_id("a").expect("div#a"); let b = tree.get_element_by_id("b").expect("div#b"); // Document-rooted: sees both spans. assert_eq!(tree.query_selector_all(".x").unwrap().len(), 2); // Scoped to #a: only its descendant span. let in_a = tree.query_selector_all_from(a, ".x").unwrap(); assert_eq!(in_a.len(), 1); let in_b = tree.query_selector_all_from(b, ".x").unwrap(); assert_eq!(in_b.len(), 1); assert_ne!(in_a[0], in_b[0]); } #[test] fn test_query_selector_from_returns_first_in_subtree_only() { let tree = parse_html( r#"

first

second

outside

"#, ); let s = tree.get_element_by_id("s").expect("section#s"); // Scoped to #s: skip the outside paragraph; return the first inside. let first_in_s = tree.query_selector_from(s, "p").unwrap().expect("a p inside"); assert_eq!(tree.text_content(first_in_s), "first"); } #[test] fn test_query_selector_from_excludes_self() { // The root element itself must not match its own scoped query, per // the spec: querySelector matches descendants only. let tree = parse_html(r#"
child
"#); let root = tree.get_element_by_id("root").expect("div#root"); // Only descendants are candidates: `.x` on root finds nothing. assert!(tree.query_selector_from(root, ".x").unwrap().is_none()); // `span` finds the child. assert!(tree.query_selector_from(root, "span").unwrap().is_some()); } }