diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 3e8dd5f95b36..4fe4f20531c8 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -87,6 +87,7 @@ use net_traits::request::CorsSettings; use ref_filter_map::ref_filter_map; use script_layout_interface::message::ReflowGoal; use script_thread::ScriptThread; +use selectors::Element as SelectorsElement; use selectors::attr::{AttrSelectorOperation, NamespaceConstraint, CaseSensitivity}; use selectors::matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext, MatchingMode}; use selectors::matching::{HAS_EDGE_CHILD_SELECTOR, HAS_SLOW_SELECTOR, HAS_SLOW_SELECTOR_LATER_SIBLINGS}; @@ -2188,10 +2189,12 @@ impl ElementMethods for Element { Err(_) => Err(Error::Syntax), Ok(selectors) => { let quirks_mode = document_from_node(self).quirks_mode(); + let root = DomRoot::from_ref(self); // FIXME(bholley): Consider an nth-index cache here. let mut ctx = MatchingContext::new(MatchingMode::Normal, None, None, quirks_mode); - Ok(matches_selector_list(&selectors, &DomRoot::from_ref(self), &mut ctx)) + ctx.scope_element = Some(root.opaque()); + Ok(matches_selector_list(&selectors, &root, &mut ctx)) } } } @@ -2206,6 +2209,7 @@ impl ElementMethods for Element { match SelectorParser::parse_author_origin_no_namespace(&selectors) { Err(_) => Err(Error::Syntax), Ok(selectors) => { + let self_root = DomRoot::from_ref(self); let root = self.upcast::(); for element in root.inclusive_ancestors() { if let Some(element) = DomRoot::downcast::(element) { @@ -2213,6 +2217,7 @@ impl ElementMethods for Element { // FIXME(bholley): Consider an nth-index cache here. let mut ctx = MatchingContext::new(MatchingMode::Normal, None, None, quirks_mode); + ctx.scope_element = Some(self_root.opaque()); if matches_selector_list(&selectors, &element, &mut ctx) { return Ok(Some(element)); } @@ -2497,7 +2502,7 @@ impl VirtualMethods for Element { } } -impl<'a> ::selectors::Element for DomRoot { +impl<'a> SelectorsElement for DomRoot { type Impl = SelectorImpl; fn opaque(&self) -> ::selectors::OpaqueElement { diff --git a/components/selectors/builder.rs b/components/selectors/builder.rs index 99a8f0869156..c996a6a6491a 100644 --- a/components/selectors/builder.rs +++ b/components/selectors/builder.rs @@ -284,7 +284,7 @@ fn complex_selector_specificity(mut iter: slice::Iter>) Component::FirstChild | Component::LastChild | Component::OnlyChild | Component::Root | - Component::Empty | + Component::Empty | Component::Scope | Component::NthChild(..) | Component::NthLastChild(..) | Component::NthOfType(..) | diff --git a/components/selectors/context.rs b/components/selectors/context.rs index c67ef6638cc6..894cc31b15db 100644 --- a/components/selectors/context.rs +++ b/components/selectors/context.rs @@ -5,6 +5,7 @@ use attr::CaseSensitivity; use bloom::BloomFilter; use nth_index_cache::NthIndexCache; +use tree::OpaqueElement; /// What kind of selector matching mode we should use. /// @@ -88,6 +89,19 @@ pub struct MatchingContext<'a> { /// only.) pub relevant_link_found: bool, + /// The element which is going to match :scope pseudo-class. It can be + /// either one :scope element, or the scoping element. + /// + /// Note that, although in theory there can be multiple :scope elements, + /// in current specs, at most one is specified, and when there is one, + /// scoping element is not relevant anymore, so we use a single field for + /// them. + /// + /// When this is None, :scope will match the root element. + /// + /// See https://drafts.csswg.org/selectors-4/#scope-pseudo + pub scope_element: Option, + quirks_mode: QuirksMode, classes_and_ids_case_sensitivity: CaseSensitivity, } @@ -125,6 +139,7 @@ impl<'a> MatchingContext<'a> { quirks_mode, relevant_link_found: false, classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(), + scope_element: None, } } diff --git a/components/selectors/matching.rs b/components/selectors/matching.rs index ff0dd40b484c..875ac2862e66 100644 --- a/components/selectors/matching.rs +++ b/components/selectors/matching.rs @@ -724,6 +724,12 @@ fn matches_simple_selector( flags_setter(element, HAS_EMPTY_SELECTOR); element.is_empty() } + Component::Scope => { + match context.shared.scope_element { + Some(ref scope_element) => element.opaque() == *scope_element, + None => element.is_root(), + } + } Component::NthChild(a, b) => { matches_generic_nth_child(element, context, a, b, false, false, flags_setter) } diff --git a/components/selectors/parser.rs b/components/selectors/parser.rs index b020a04fd032..44ff450aa3e0 100644 --- a/components/selectors/parser.rs +++ b/components/selectors/parser.rs @@ -673,6 +673,7 @@ pub enum Component { FirstChild, LastChild, OnlyChild, Root, Empty, + Scope, NthChild(i32, i32), NthLastChild(i32, i32), NthOfType(i32, i32), @@ -969,6 +970,7 @@ impl ToCss for Component { OnlyChild => dest.write_str(":only-child"), Root => dest.write_str(":root"), Empty => dest.write_str(":empty"), + Scope => dest.write_str(":scope"), FirstOfType => dest.write_str(":first-of-type"), LastOfType => dest.write_str(":last-of-type"), OnlyOfType => dest.write_str(":only-of-type"), @@ -1699,6 +1701,7 @@ fn parse_simple_pseudo_class<'i, P, E, Impl>(parser: &P, name: CowRcStr<'i>) "only-child" => Ok(Component::OnlyChild), "root" => Ok(Component::Root), "empty" => Ok(Component::Empty), + "scope" => Ok(Component::Scope), "first-of-type" => Ok(Component::FirstOfType), "last-of-type" => Ok(Component::LastOfType), "only-of-type" => Ok(Component::OnlyOfType), diff --git a/ports/geckolib/glue.rs b/ports/geckolib/glue.rs index 06c7b794a962..1c975c5767b0 100644 --- a/ports/geckolib/glue.rs +++ b/ports/geckolib/glue.rs @@ -1530,6 +1530,7 @@ pub unsafe extern "C" fn Servo_SelectorList_Matches( None, element.owner_document_quirks_mode(), ); + context.scope_element = Some(element.opaque()); selectors::matching::matches_selector_list(selectors, &element, &mut context) } diff --git a/tests/wpt/metadata/MANIFEST.json b/tests/wpt/metadata/MANIFEST.json index ff6334f4d99b..8cb6669ace21 100644 --- a/tests/wpt/metadata/MANIFEST.json +++ b/tests/wpt/metadata/MANIFEST.json @@ -152045,6 +152045,18 @@ {} ] ], + "css/selectors4/scope-without-scoping.html": [ + [ + "/css/selectors4/scope-without-scoping.html", + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "css/selectors4/selector-required.html": [ [ "/css/selectors4/selector-required.html", @@ -538672,6 +538684,10 @@ "607553f41a33ce3630752cdf027c9f904833a19d", "reftest" ], + "css/selectors4/scope-without-scoping.html": [ + "f70b8d60543c5a28fcf955b1780f15c03d60991a", + "reftest" + ], "css/selectors4/selector-required-ref.html": [ "815bc765614b4c2e3d8f8f6303e6bb2ee0989c23", "support" @@ -570101,7 +570117,7 @@ "testharness" ], "dom/nodes/Element-closest.html": [ - "4171fb8b70948ba2617e05b118aaf5d9367e916f", + "5abddb81959019267d8b69002ee95b011b2fe34a", "testharness" ], "dom/nodes/Element-firstElementChild-entity-xhtml.xhtml": [ diff --git a/tests/wpt/metadata/dom/nodes/Element-closest.html.ini b/tests/wpt/metadata/dom/nodes/Element-closest.html.ini index ff8b84fd2dac..73e8c643d680 100644 --- a/tests/wpt/metadata/dom/nodes/Element-closest.html.ini +++ b/tests/wpt/metadata/dom/nodes/Element-closest.html.ini @@ -4,3 +4,6 @@ bug: https://github.com/servo/servo/issues/10781 expected: FAIL + [Element.closest with context node 'test4' and selector ':has(> :scope)'] + expected: FAIL + diff --git a/tests/wpt/web-platform-tests/css/selectors4/scope-without-scoping.html b/tests/wpt/web-platform-tests/css/selectors4/scope-without-scoping.html new file mode 100644 index 000000000000..2ee7619e8023 --- /dev/null +++ b/tests/wpt/web-platform-tests/css/selectors4/scope-without-scoping.html @@ -0,0 +1,23 @@ + + + +Selectors Level 4: :scope without scoping + + + + + + +

Test passes if there is a filled green square and no red.

+
+ + diff --git a/tests/wpt/web-platform-tests/dom/nodes/Element-closest.html b/tests/wpt/web-platform-tests/dom/nodes/Element-closest.html index e5af1a43b629..386e3bd251bd 100644 --- a/tests/wpt/web-platform-tests/dom/nodes/Element-closest.html +++ b/tests/wpt/web-platform-tests/dom/nodes/Element-closest.html @@ -56,11 +56,10 @@ do_test(":first-child" , "test12", "test3"); do_test(":invalid" , "test11", "test2"); - do_scope_test(":scope" , "test4"); - do_scope_test("select > :scope" , "test4"); - do_scope_test("div > :scope" , "test4"); - do_scope_test(":has(> :scope)" , "test4"); - + do_test(":scope" , "test4", "test4"); + do_test("select > :scope" , "test4", "test4"); + do_test("div > :scope" , "test4", ""); + do_test(":has(> :scope)" , "test4", "test3"); function do_test(aSelector, aElementId, aTargetId) { test(function() { var el = document.getElementById(aElementId).closest(aSelector); @@ -71,13 +70,4 @@ } }, "Element.closest with context node '" + aElementId + "' and selector '" + aSelector + "'"); } - -function do_scope_test(aSelector, aElementId) { - test(function() { - var el = document.getElementById(aElementId); - assert_throws("SYNTAX_ERR", function() { - el.closest(aSelector); - }); - }, "Element.closest with context node '" + aElementId + "' and selector '" + aSelector + "' should throw"); -}