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
where Impl: SelectorImpl
{
fn simple_selector_specificity<Impl>(simple_selector: &Component<Impl>,
specificity: &mut Specificity)
where Impl: SelectorImpl
fn simple_selector_specificity<Impl>(
simple_selector: &Component<Impl>,
specificity: &mut Specificity,
)
where
Impl: SelectorImpl
{
match *simple_selector {
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::LocalName(..) => {
specificity.element_selectors += 1

View file

@ -393,6 +393,9 @@ where
element.parent_element()
}
Combinator::SlotAssignment => {
element.assigned_slot()
}
Combinator::PseudoElement => {
element.pseudo_element_originating_element()
}
@ -453,6 +456,7 @@ where
}
Combinator::Child |
Combinator::Descendant |
Combinator::SlotAssignment |
Combinator::PseudoElement => {
SelectorMatchingResult::NotMatchedGlobally
}
@ -541,6 +545,21 @@ where
{
match *selector {
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) => {
element.match_pseudo_element(pseudo, context.shared)
}

View file

@ -132,6 +132,11 @@ pub trait Parser<'i> {
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
/// pseudo-elements.
fn parse_non_ts_pseudo_class(
@ -353,6 +358,13 @@ impl<Impl: SelectorImpl> SelectorMethods for Component<Impl> {
}
match *self {
Slotted(ref selectors) => {
for selector in selectors.iter() {
if !selector.visit(visitor) {
return false;
}
}
}
Negation(ref negated) => {
for component in negated.iter() {
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
/// for revalidation selectors.
PseudoElement,
/// Another combinator used for ::slotted(), which represent the jump from
/// a node to its assigned slot.
SlotAssignment,
}
impl Combinator {
/// Returns true if this combinator is a child or descendant combinator.
#[inline]
pub fn is_ancestor(&self) -> bool {
matches!(*self, Combinator::Child |
Combinator::Descendant |
Combinator::PseudoElement)
matches!(*self,
Combinator::Child |
Combinator::Descendant |
Combinator::PseudoElement |
Combinator::SlotAssignment)
}
/// 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.
AttributeOther(Box<AttrSelectorWithNamespace<Impl>>),
// Pseudo-classes
//
// 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
// we represent as a namespace and a localname.
//
// Note: if/when we upgrade this to CSS4, which supports combinators, we
// need to think about how this should interact with visit_complex_selector,
// and what the consumers of those APIs should do about the presence of
// combinators in negation.
/// Pseudo-classes
///
/// 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 we represent as a namespace and a localname.
///
/// Note: if/when we upgrade this to CSS4, which supports combinators, we
/// need to think about how this should interact with
/// visit_complex_selector, and what the consumers of those APIs should do
/// about the presence of combinators in negation.
Negation(Box<[Component<Impl>]>),
FirstChild, LastChild, OnlyChild,
Root,
@ -739,6 +756,13 @@ pub enum Component<Impl: SelectorImpl> {
LastOfType,
OnlyOfType,
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),
}
@ -868,13 +892,15 @@ impl<Impl: SelectorImpl> ToCss for Selector<Impl> {
let mut perform_step_2 = true;
if first_non_namespace == compound.len() - 1 {
match (combinators.peek(), &compound[first_non_namespace]) {
// We have to be careful here, because if there is a pseudo
// element "combinator" there isn't really just the one
// simple selector. Technically this compound selector
// contains the pseudo element selector as
// well--Combinator::PseudoElement doesn't exist in the
// We have to be careful here, because if there is a
// pseudo element "combinator" there isn't really just
// the one simple selector. Technically this compound
// selector contains the pseudo element selector as well
// -- Combinator::PseudoElement, just like
// Combinator::SlotAssignment, don't exist in the
// spec.
(Some(&&Component::Combinator(Combinator::PseudoElement)), _) => (),
(Some(&&Component::Combinator(Combinator::PseudoElement)), _) |
(Some(&&Component::Combinator(Combinator::SlotAssignment)), _) => (),
(_, &Component::ExplicitUniversalType) => {
// Iterate over everything so we serialize the namespace
// too.
@ -942,6 +968,7 @@ impl ToCss for Combinator {
Combinator::NextSibling => dest.write_str(" + "),
Combinator::LaterSibling => dest.write_str(" ~ "),
Combinator::PseudoElement => Ok(()),
Combinator::SlotAssignment => Ok(()),
}
}
}
@ -970,6 +997,16 @@ impl<Impl: SelectorImpl> ToCss for Component<Impl> {
Combinator(ref c) => {
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) => {
p.to_css(dest)
}
@ -1120,10 +1157,14 @@ where
let mut builder = SelectorBuilder::default();
let mut has_pseudo_element;
let mut slotted;
'outer_loop: loop {
// Parse a sequence of simple selectors.
has_pseudo_element = match parse_compound_selector(parser, input, &mut builder)? {
Some(has_pseudo_element) => has_pseudo_element,
match parse_compound_selector(parser, input, &mut builder)? {
Some((has_pseudo, slot)) => {
has_pseudo_element = has_pseudo;
slotted = slot;
}
None => {
return Err(input.new_custom_error(if builder.has_combinators() {
SelectorParseErrorKind::DanglingCombinator
@ -1132,7 +1173,8 @@ where
}))
}
};
if has_pseudo_element {
if has_pseudo_element || slotted {
break;
}
@ -1263,6 +1305,7 @@ where
enum SimpleSelectorParseResult<Impl: SelectorImpl> {
SimpleSelector(Component<Impl>),
PseudoElement(Impl::PseudoElement),
SlottedPseudo(Box<[Selector<Impl>]>),
}
#[derive(Debug)]
@ -1575,7 +1618,8 @@ where
None => {
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));
}
}
@ -1592,12 +1636,13 @@ where
/// `Err(())` means invalid 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>(
parser: &P,
input: &mut CssParser<'i, 't>,
builder: &mut SelectorBuilder<Impl>,
) -> Result<Option<bool>, ParseError<'i, P::Error>>
) -> Result<Option<(bool, bool)>, ParseError<'i, P::Error>>
where
P: Parser<'i, Impl=Impl>,
Impl: SelectorImpl,
@ -1605,6 +1650,7 @@ where
input.skip_whitespace();
let mut empty = true;
let mut slot = false;
if !parse_type_selector(parser, input, builder)? {
if let Some(url) = parser.default_namespace() {
// If there was no explicit type selector, but there is a
@ -1618,13 +1664,18 @@ where
let mut pseudo = false;
loop {
match parse_one_simple_selector(parser, input, /* inside_negation = */ false)? {
None => break,
Some(SimpleSelectorParseResult::SimpleSelector(s)) => {
let parse_result =
match parse_one_simple_selector(parser, input, /* inside_negation = */ false)? {
None => break,
Some(result) => result,
};
match parse_result {
SimpleSelectorParseResult::SimpleSelector(s) => {
builder.push_simple_selector(s);
empty = false
}
Some(SimpleSelectorParseResult::PseudoElement(p)) => {
SimpleSelectorParseResult::PseudoElement(p) => {
// Try to parse state to its right. There are only 3 allowable
// state selectors that can go on pseudo-elements.
let mut state_selectors = SmallVec::<[Component<Impl>; 3]>::new();
@ -1673,13 +1724,25 @@ where
empty = false;
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 {
// An empty selector is invalid.
Ok(None)
} else {
Ok(Some(pseudo))
Ok(Some((pseudo, slot)))
}
}
@ -1790,14 +1853,33 @@ where
let is_pseudo_element = !is_single_colon ||
P::pseudo_element_allows_single_colon(&name);
if is_pseudo_element {
let pseudo_element = if is_functional {
input.parse_nested_block(|input| {
P::parse_functional_pseudo_element(parser, name, input)
})?
let parse_result = if is_functional {
if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") {
SimpleSelectorParseResult::SlottedPseudo(
input.parse_nested_block(|input| {
parse_compound_selector_list(
parser,
input,
)
})?
)
} else {
SimpleSelectorParseResult::PseudoElement(
input.parse_nested_block(|input| {
P::parse_functional_pseudo_element(
parser,
name,
input,
)
})?
)
}
} else {
P::parse_pseudo_element(parser, location, name)?
SimpleSelectorParseResult::PseudoElement(
P::parse_pseudo_element(parser, location, name)?
)
};
Ok(Some(SimpleSelectorParseResult::PseudoElement(pseudo_element)))
Ok(Some(parse_result))
} else {
let pseudo_class = if is_functional {
input.parse_nested_block(|input| {
@ -1979,6 +2061,10 @@ pub mod tests {
type Impl = DummySelectorImpl;
type Error = SelectorParseErrorKind<'i>;
fn parse_slotted(&self) -> bool {
true
}
fn parse_non_ts_pseudo_class(
&self,
location: SourceLocation,
@ -2085,9 +2171,9 @@ pub mod tests {
#[test]
fn test_parsing() {
assert!(parse("").is_err()) ;
assert!(parse(":lang(4)").is_err()) ;
assert!(parse(":lang(en US)").is_err()) ;
assert!(parse("").is_err());
assert!(parse(":lang(4)").is_err());
assert!(parse(":lang(en US)").is_err());
assert_eq!(parse("EeÉ"), Ok(SelectorList::from_vec(vec!(
Selector::from_vec(vec!(
Component::LocalName(LocalName {
@ -2394,6 +2480,16 @@ pub mod tests {
].into_boxed_slice()
)), 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]

View file

@ -84,6 +84,13 @@ pub trait Element: Sized + Clone + Debug {
/// Whether this element is a `link`.
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,
id: &<Self::Impl as SelectorImpl>::Identifier,
case_sensitivity: CaseSensitivity)