selectors: Add parsing support for ::slotted().

Without turning it on yet, of course.

The reason why I didn't use the general PseudoElement mechanism is because this
pseudo is a bit of its own thing, and I found easier to make ::selectors know
about it (because you need to jump to the assigned slot) than the other way
around.

Also, we need to support ::slotted(..)::before and such, and supporting multiple
pseudo-elements like that breaks some other invariants around the SelectorMap,
and fixing those would require special-casing slotted a lot more in other parts
of the code.

Let me know if you think otherwise.

I also don't like much the boolean tuple return value, but I plan to do some
cleanup in the area in a bit, so it should go away soon, I'd hope.
This commit is contained in:
Emilio Cobos Álvarez 2017-12-10 14:23:27 +01:00
parent 0fa605d243
commit 7886e033aa
No known key found for this signature in database
GPG key ID: 056B727BB9C1027C
4 changed files with 175 additions and 42 deletions

View file

@ -264,12 +264,23 @@ fn complex_selector_specificity<Impl>(mut iter: slice::Iter<Component<Impl>>)
-> Specificity -> Specificity
where Impl: SelectorImpl where Impl: SelectorImpl
{ {
fn simple_selector_specificity<Impl>(simple_selector: &Component<Impl>, fn simple_selector_specificity<Impl>(
specificity: &mut Specificity) simple_selector: &Component<Impl>,
where Impl: SelectorImpl specificity: &mut Specificity,
)
where
Impl: SelectorImpl
{ {
match *simple_selector { match *simple_selector {
Component::Combinator(..) => unreachable!(), Component::Combinator(..) => unreachable!(),
// FIXME(emilio): Spec doesn't define any particular specificity for
// ::slotted(), so apply the general rule for pseudos per:
//
// https://github.com/w3c/csswg-drafts/issues/1915
//
// Though other engines compute it dynamically, so maybe we should
// do that instead, eventually.
Component::Slotted(..) |
Component::PseudoElement(..) | Component::PseudoElement(..) |
Component::LocalName(..) => { Component::LocalName(..) => {
specificity.element_selectors += 1 specificity.element_selectors += 1

View file

@ -393,6 +393,9 @@ where
element.parent_element() element.parent_element()
} }
Combinator::SlotAssignment => {
element.assigned_slot()
}
Combinator::PseudoElement => { Combinator::PseudoElement => {
element.pseudo_element_originating_element() element.pseudo_element_originating_element()
} }
@ -453,6 +456,7 @@ where
} }
Combinator::Child | Combinator::Child |
Combinator::Descendant | Combinator::Descendant |
Combinator::SlotAssignment |
Combinator::PseudoElement => { Combinator::PseudoElement => {
SelectorMatchingResult::NotMatchedGlobally SelectorMatchingResult::NotMatchedGlobally
} }
@ -541,6 +545,21 @@ where
{ {
match *selector { match *selector {
Component::Combinator(_) => unreachable!(), Component::Combinator(_) => unreachable!(),
Component::Slotted(ref selectors) => {
context.shared.nesting_level += 1;
let result =
element.assigned_slot().is_some() &&
selectors.iter().any(|s| {
matches_complex_selector(
s.iter(),
element,
context.shared,
flags_setter,
)
});
context.shared.nesting_level -= 1;
result
}
Component::PseudoElement(ref pseudo) => { Component::PseudoElement(ref pseudo) => {
element.match_pseudo_element(pseudo, context.shared) element.match_pseudo_element(pseudo, context.shared)
} }

View file

@ -132,6 +132,11 @@ pub trait Parser<'i> {
is_css2_pseudo_element(name) is_css2_pseudo_element(name)
} }
/// Whether to parse the `::slotted()` pseudo-element.
fn parse_slotted(&self) -> bool {
false
}
/// This function can return an "Err" pseudo-element in order to support CSS2.1 /// This function can return an "Err" pseudo-element in order to support CSS2.1
/// pseudo-elements. /// pseudo-elements.
fn parse_non_ts_pseudo_class( fn parse_non_ts_pseudo_class(
@ -353,6 +358,13 @@ impl<Impl: SelectorImpl> SelectorMethods for Component<Impl> {
} }
match *self { match *self {
Slotted(ref selectors) => {
for selector in selectors.iter() {
if !selector.visit(visitor) {
return false;
}
}
}
Negation(ref negated) => { Negation(ref negated) => {
for component in negated.iter() { for component in negated.iter() {
if !component.visit(visitor) { if !component.visit(visitor) {
@ -658,15 +670,20 @@ pub enum Combinator {
/// combinator for this, we will need to fix up the way hashes are computed /// combinator for this, we will need to fix up the way hashes are computed
/// for revalidation selectors. /// for revalidation selectors.
PseudoElement, PseudoElement,
/// Another combinator used for ::slotted(), which represent the jump from
/// a node to its assigned slot.
SlotAssignment,
} }
impl Combinator { impl Combinator {
/// Returns true if this combinator is a child or descendant combinator. /// Returns true if this combinator is a child or descendant combinator.
#[inline] #[inline]
pub fn is_ancestor(&self) -> bool { pub fn is_ancestor(&self) -> bool {
matches!(*self, Combinator::Child | matches!(*self,
Combinator::Child |
Combinator::Descendant | Combinator::Descendant |
Combinator::PseudoElement) Combinator::PseudoElement |
Combinator::SlotAssignment)
} }
/// Returns true if this combinator is a pseudo-element combinator. /// Returns true if this combinator is a pseudo-element combinator.
@ -716,16 +733,16 @@ pub enum Component<Impl: SelectorImpl> {
// Use a Box in the less common cases with more data to keep size_of::<Component>() small. // Use a Box in the less common cases with more data to keep size_of::<Component>() small.
AttributeOther(Box<AttrSelectorWithNamespace<Impl>>), AttributeOther(Box<AttrSelectorWithNamespace<Impl>>),
// Pseudo-classes /// Pseudo-classes
// ///
// CSS3 Negation only takes a simple simple selector, but we still need to /// CSS3 Negation only takes a simple simple selector, but we still need to
// treat it as a compound selector because it might be a type selector which /// treat it as a compound selector because it might be a type selector
// we represent as a namespace and a localname. /// which we represent as a namespace and a localname.
// ///
// Note: if/when we upgrade this to CSS4, which supports combinators, we /// Note: if/when we upgrade this to CSS4, which supports combinators, we
// need to think about how this should interact with visit_complex_selector, /// need to think about how this should interact with
// and what the consumers of those APIs should do about the presence of /// visit_complex_selector, and what the consumers of those APIs should do
// combinators in negation. /// about the presence of combinators in negation.
Negation(Box<[Component<Impl>]>), Negation(Box<[Component<Impl>]>),
FirstChild, LastChild, OnlyChild, FirstChild, LastChild, OnlyChild,
Root, Root,
@ -739,6 +756,13 @@ pub enum Component<Impl: SelectorImpl> {
LastOfType, LastOfType,
OnlyOfType, OnlyOfType,
NonTSPseudoClass(Impl::NonTSPseudoClass), NonTSPseudoClass(Impl::NonTSPseudoClass),
/// The ::slotted() pseudo-element (which isn't actually a pseudo-element,
/// and probably should be a pseudo-class):
///
/// https://drafts.csswg.org/css-scoping/#slotted-pseudo
///
/// The selectors here are compound selectors, that is, no combinators.
Slotted(Box<[Selector<Impl>]>),
PseudoElement(Impl::PseudoElement), PseudoElement(Impl::PseudoElement),
} }
@ -868,13 +892,15 @@ impl<Impl: SelectorImpl> ToCss for Selector<Impl> {
let mut perform_step_2 = true; let mut perform_step_2 = true;
if first_non_namespace == compound.len() - 1 { if first_non_namespace == compound.len() - 1 {
match (combinators.peek(), &compound[first_non_namespace]) { match (combinators.peek(), &compound[first_non_namespace]) {
// We have to be careful here, because if there is a pseudo // We have to be careful here, because if there is a
// element "combinator" there isn't really just the one // pseudo element "combinator" there isn't really just
// simple selector. Technically this compound selector // the one simple selector. Technically this compound
// contains the pseudo element selector as // selector contains the pseudo element selector as well
// well--Combinator::PseudoElement doesn't exist in the // -- Combinator::PseudoElement, just like
// Combinator::SlotAssignment, don't exist in the
// spec. // spec.
(Some(&&Component::Combinator(Combinator::PseudoElement)), _) => (), (Some(&&Component::Combinator(Combinator::PseudoElement)), _) |
(Some(&&Component::Combinator(Combinator::SlotAssignment)), _) => (),
(_, &Component::ExplicitUniversalType) => { (_, &Component::ExplicitUniversalType) => {
// Iterate over everything so we serialize the namespace // Iterate over everything so we serialize the namespace
// too. // too.
@ -942,6 +968,7 @@ impl ToCss for Combinator {
Combinator::NextSibling => dest.write_str(" + "), Combinator::NextSibling => dest.write_str(" + "),
Combinator::LaterSibling => dest.write_str(" ~ "), Combinator::LaterSibling => dest.write_str(" ~ "),
Combinator::PseudoElement => Ok(()), Combinator::PseudoElement => Ok(()),
Combinator::SlotAssignment => Ok(()),
} }
} }
} }
@ -970,6 +997,16 @@ impl<Impl: SelectorImpl> ToCss for Component<Impl> {
Combinator(ref c) => { Combinator(ref c) => {
c.to_css(dest) c.to_css(dest)
} }
Slotted(ref selectors) => {
dest.write_str("::slotted(")?;
let mut iter = selectors.iter();
iter.next().expect("At least one selector").to_css(dest)?;
for other in iter {
dest.write_str(", ")?;
other.to_css(dest)?;
}
dest.write_char(')')
}
PseudoElement(ref p) => { PseudoElement(ref p) => {
p.to_css(dest) p.to_css(dest)
} }
@ -1120,10 +1157,14 @@ where
let mut builder = SelectorBuilder::default(); let mut builder = SelectorBuilder::default();
let mut has_pseudo_element; let mut has_pseudo_element;
let mut slotted;
'outer_loop: loop { 'outer_loop: loop {
// Parse a sequence of simple selectors. // Parse a sequence of simple selectors.
has_pseudo_element = match parse_compound_selector(parser, input, &mut builder)? { match parse_compound_selector(parser, input, &mut builder)? {
Some(has_pseudo_element) => has_pseudo_element, Some((has_pseudo, slot)) => {
has_pseudo_element = has_pseudo;
slotted = slot;
}
None => { None => {
return Err(input.new_custom_error(if builder.has_combinators() { return Err(input.new_custom_error(if builder.has_combinators() {
SelectorParseErrorKind::DanglingCombinator SelectorParseErrorKind::DanglingCombinator
@ -1132,7 +1173,8 @@ where
})) }))
} }
}; };
if has_pseudo_element {
if has_pseudo_element || slotted {
break; break;
} }
@ -1263,6 +1305,7 @@ where
enum SimpleSelectorParseResult<Impl: SelectorImpl> { enum SimpleSelectorParseResult<Impl: SelectorImpl> {
SimpleSelector(Component<Impl>), SimpleSelector(Component<Impl>),
PseudoElement(Impl::PseudoElement), PseudoElement(Impl::PseudoElement),
SlottedPseudo(Box<[Selector<Impl>]>),
} }
#[derive(Debug)] #[derive(Debug)]
@ -1575,7 +1618,8 @@ where
None => { None => {
return Err(input.new_custom_error(SelectorParseErrorKind::EmptyNegation)); return Err(input.new_custom_error(SelectorParseErrorKind::EmptyNegation));
}, },
Some(SimpleSelectorParseResult::PseudoElement(_)) => { Some(SimpleSelectorParseResult::PseudoElement(_)) |
Some(SimpleSelectorParseResult::SlottedPseudo(_)) => {
return Err(input.new_custom_error(SelectorParseErrorKind::NonSimpleSelectorInNegation)); return Err(input.new_custom_error(SelectorParseErrorKind::NonSimpleSelectorInNegation));
} }
} }
@ -1592,12 +1636,13 @@ where
/// `Err(())` means invalid selector. /// `Err(())` means invalid selector.
/// `Ok(None)` is an empty selector /// `Ok(None)` is an empty selector
/// ///
/// The boolean represent whether a pseudo-element has been parsed. /// The booleans represent whether a pseudo-element has been parsed, and whether
/// ::slotted() has been parsed, respectively.
fn parse_compound_selector<'i, 't, P, Impl>( fn parse_compound_selector<'i, 't, P, Impl>(
parser: &P, parser: &P,
input: &mut CssParser<'i, 't>, input: &mut CssParser<'i, 't>,
builder: &mut SelectorBuilder<Impl>, builder: &mut SelectorBuilder<Impl>,
) -> Result<Option<bool>, ParseError<'i, P::Error>> ) -> Result<Option<(bool, bool)>, ParseError<'i, P::Error>>
where where
P: Parser<'i, Impl=Impl>, P: Parser<'i, Impl=Impl>,
Impl: SelectorImpl, Impl: SelectorImpl,
@ -1605,6 +1650,7 @@ where
input.skip_whitespace(); input.skip_whitespace();
let mut empty = true; let mut empty = true;
let mut slot = false;
if !parse_type_selector(parser, input, builder)? { if !parse_type_selector(parser, input, builder)? {
if let Some(url) = parser.default_namespace() { if let Some(url) = parser.default_namespace() {
// If there was no explicit type selector, but there is a // If there was no explicit type selector, but there is a
@ -1618,13 +1664,18 @@ where
let mut pseudo = false; let mut pseudo = false;
loop { loop {
let parse_result =
match parse_one_simple_selector(parser, input, /* inside_negation = */ false)? { match parse_one_simple_selector(parser, input, /* inside_negation = */ false)? {
None => break, None => break,
Some(SimpleSelectorParseResult::SimpleSelector(s)) => { Some(result) => result,
};
match parse_result {
SimpleSelectorParseResult::SimpleSelector(s) => {
builder.push_simple_selector(s); builder.push_simple_selector(s);
empty = false empty = false
} }
Some(SimpleSelectorParseResult::PseudoElement(p)) => { SimpleSelectorParseResult::PseudoElement(p) => {
// Try to parse state to its right. There are only 3 allowable // Try to parse state to its right. There are only 3 allowable
// state selectors that can go on pseudo-elements. // state selectors that can go on pseudo-elements.
let mut state_selectors = SmallVec::<[Component<Impl>; 3]>::new(); let mut state_selectors = SmallVec::<[Component<Impl>; 3]>::new();
@ -1673,13 +1724,25 @@ where
empty = false; empty = false;
break break
} }
SimpleSelectorParseResult::SlottedPseudo(selectors) => {
empty = false;
slot = true;
if !builder.is_empty() {
builder.push_combinator(Combinator::SlotAssignment);
}
builder.push_simple_selector(Component::Slotted(selectors));
// FIXME(emilio): ::slotted() should support ::before and
// ::after after it, so we shouldn't break, but we shouldn't
// push more type selectors either.
break;
}
} }
} }
if empty { if empty {
// An empty selector is invalid. // An empty selector is invalid.
Ok(None) Ok(None)
} else { } else {
Ok(Some(pseudo)) Ok(Some((pseudo, slot)))
} }
} }
@ -1790,14 +1853,33 @@ where
let is_pseudo_element = !is_single_colon || let is_pseudo_element = !is_single_colon ||
P::pseudo_element_allows_single_colon(&name); P::pseudo_element_allows_single_colon(&name);
if is_pseudo_element { if is_pseudo_element {
let pseudo_element = if is_functional { let parse_result = if is_functional {
if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") {
SimpleSelectorParseResult::SlottedPseudo(
input.parse_nested_block(|input| { input.parse_nested_block(|input| {
P::parse_functional_pseudo_element(parser, name, input) parse_compound_selector_list(
parser,
input,
)
})? })?
)
} else { } else {
SimpleSelectorParseResult::PseudoElement(
input.parse_nested_block(|input| {
P::parse_functional_pseudo_element(
parser,
name,
input,
)
})?
)
}
} else {
SimpleSelectorParseResult::PseudoElement(
P::parse_pseudo_element(parser, location, name)? P::parse_pseudo_element(parser, location, name)?
)
}; };
Ok(Some(SimpleSelectorParseResult::PseudoElement(pseudo_element))) Ok(Some(parse_result))
} else { } else {
let pseudo_class = if is_functional { let pseudo_class = if is_functional {
input.parse_nested_block(|input| { input.parse_nested_block(|input| {
@ -1979,6 +2061,10 @@ pub mod tests {
type Impl = DummySelectorImpl; type Impl = DummySelectorImpl;
type Error = SelectorParseErrorKind<'i>; type Error = SelectorParseErrorKind<'i>;
fn parse_slotted(&self) -> bool {
true
}
fn parse_non_ts_pseudo_class( fn parse_non_ts_pseudo_class(
&self, &self,
location: SourceLocation, location: SourceLocation,
@ -2394,6 +2480,16 @@ pub mod tests {
].into_boxed_slice() ].into_boxed_slice()
)), specificity(0, 0, 0)) )), specificity(0, 0, 0))
)))); ))));
assert!(parse("::slotted()").is_err());
assert!(parse("::slotted(div)").is_ok());
assert!(parse("::slotted(div).foo").is_err());
assert!(parse("::slotted(div + bar)").is_err());
assert!(parse("::slotted(div) + foo").is_err());
assert!(parse("div ::slotted(div)").is_ok());
assert!(parse("div + slot::slotted(div)").is_ok());
assert!(parse("div + slot::slotted(div.foo)").is_ok());
assert!(parse("div + slot::slotted(.foo, bar, .baz)").is_ok());
} }
#[test] #[test]

View file

@ -84,6 +84,13 @@ pub trait Element: Sized + Clone + Debug {
/// Whether this element is a `link`. /// Whether this element is a `link`.
fn is_link(&self) -> bool; fn is_link(&self) -> bool;
/// Returns the assigned <slot> element this element is assigned to.
///
/// Necessary for the `::slotted` pseudo-class.
fn assigned_slot(&self) -> Option<Self> {
None
}
fn has_id(&self, fn has_id(&self,
id: &<Self::Impl as SelectorImpl>::Identifier, id: &<Self::Impl as SelectorImpl>::Identifier,
case_sensitivity: CaseSensitivity) case_sensitivity: CaseSensitivity)