diff --git a/crates/obscura-dom/src/selector.rs b/crates/obscura-dom/src/selector.rs index 88f3917..e8e2176 100644 --- a/crates/obscura-dom/src/selector.rs +++ b/crates/obscura-dom/src/selector.rs @@ -743,6 +743,29 @@ impl DomTree { Ok(results) } + /// Test one element as the selector subject, including when it is + /// detached or is a direct child of a shadow-tree compatibility root. + pub fn matches_selector(&self, nid: NodeId, selector: &str) -> Result { + if !self.with_node(nid, |node| node.is_element()).unwrap_or(false) { + return Ok(false); + } + let selector_list = parse_selector(selector)?; + let mut caches = selectors::context::SelectorCaches::default(); + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut caches, + QuirksMode::NoQuirks, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + Ok(selectors::matching::matches_selector_list( + &selector_list, + &DomElement::new(self, nid), + &mut context, + )) + } + /// Parse a single selector once for repeated single-element matching, and /// precompute its specificity, ancestor hashes, and "subject key" (the /// rightmost id/class/tag used to bucket the rule for fast candidate diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index f58c78c..fdd1160 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -2059,13 +2059,7 @@ class Element extends Node { if (rest === "") return true; return this.matches(rest); } - const parent = this.parentNode; - if (!parent || !parent.querySelectorAll) return false; - const matches = parent.querySelectorAll(s); - for (let i = 0; i < matches.length; i++) { - if (matches[i]._nid === this._nid) return true; - } - return false; + return _dom("matches_selector", this._nid, String(s)) === "true"; } closest(s) { let el = this; diff --git a/crates/obscura-js/src/ops.rs b/crates/obscura-js/src/ops.rs index 0e56904..b8a03ba 100644 --- a/crates/obscura-js/src/ops.rs +++ b/crates/obscura-js/src/ops.rs @@ -288,6 +288,10 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str .map(|ids| ids.iter().map(|id| id.index() as i32).collect()).unwrap_or_default(); serde_json::to_string(&ids).unwrap_or("[]".into()) } + "matches_selector" => { + let nid = NodeId::new(arg1.parse::().unwrap_or(0)); + dom.matches_selector(nid, &arg2).unwrap_or(false).to_string() + } "node_type" => { let nid = arg1.parse::().unwrap_or(0); dom.with_node(NodeId::new(nid), |n| match &n.data { diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index f5ea935..2d5503f 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1991,7 +1991,10 @@ mod tests { const parsed = [ inside.parentNode === root, inside.getRootNode() === root, - root.textContent + root.textContent, + inside.matches("span#inside"), + root.querySelector("span#inside") === inside, + root.querySelectorAll("span#inside").length === 1 ]; const a = document.createElement("a"); @@ -2059,7 +2062,7 @@ mod tests { ], [true, true, true], [true, true, true], - [true, true, "inside"], + [true, true, "inside", true, true, true], ["b", "a", "i"], [["b", "a", "x-one", "x-two", "i"], 0, true, true], [["a", "x-one", "x-two", "b"], true, true],