mirror of
https://github.com/servo/servo.git
synced 2025-08-05 21:50:18 +01:00
style: Keep track of nested dependencies for :where() and :is().
The tricky part of :is() and :where() is that they can have combinators inside, so something like this is valid: foo:is(#bar > .baz) ~ taz The current invalidation logic is based on the assumption that you can represent a combinator as a (selector, offset) tuple, which are stored in the Dependency struct. This assumption breaks with :is() and :where(), so we need to make them be able to represent a combinator in an "inner" selector. For this purpose, we add a `parent` dependency. With it, when invalidating inside the `:is()` we can represent combinators inside as a stack. The basic idea is that, for the example above, when an id of "bar" is added or removed, we'd find a dependency like: Dependency { selector: #bar > .baz, offset: 1, // pointing to the `>` combinator parent: Some(Dependency { selector: foo:is(#bar > .baz) > taz, offset: 1, // Pointing to the `~` combinator. parent: None, }) } That way, we'd start matching at the element that changed, towards the right, and if we find an element that matches .baz, instead of invalidating that element, we'd look at the parent dependency, then double-check that the whole left-hand-side of the selector (foo:is(#bar > .baz)) actually changed, and then keep invalidating to the right using the parent dependency as usual. This patch only builds the data structure and keeps the code compiling, the actual invalidation work will come in a following patch. Differential Revision: https://phabricator.services.mozilla.com/D71421
This commit is contained in:
parent
cd63d7b272
commit
c1bc588c93
2 changed files with 249 additions and 150 deletions
|
@ -47,6 +47,23 @@ pub struct Dependency {
|
|||
|
||||
/// The offset into the selector that we should match on.
|
||||
pub selector_offset: usize,
|
||||
|
||||
/// The parent dependency for an ancestor selector. For example, consider
|
||||
/// the following:
|
||||
///
|
||||
/// .foo .bar:where(.baz span) .qux
|
||||
/// ^ ^ ^
|
||||
/// A B C
|
||||
///
|
||||
/// We'd generate:
|
||||
///
|
||||
/// * One dependency for .quz (offset: 0, parent: None)
|
||||
/// * One dependency for .baz pointing to B with parent being a
|
||||
/// dependency pointing to C.
|
||||
/// * One dependency from .bar pointing to C (parent: None)
|
||||
/// * One dependency from .foo pointing to A (parent: None)
|
||||
///
|
||||
pub parent: Option<Box<Dependency>>,
|
||||
}
|
||||
|
||||
/// The kind of elements down the tree this dependency may affect.
|
||||
|
@ -227,78 +244,120 @@ impl InvalidationMap {
|
|||
) -> Result<(), FailedAllocationError> {
|
||||
debug!("InvalidationMap::note_selector({:?})", selector);
|
||||
|
||||
let mut iter = selector.iter();
|
||||
let mut document_state = DocumentState::empty();
|
||||
|
||||
{
|
||||
let mut parent_stack = SmallVec::new();
|
||||
let mut alloc_error = None;
|
||||
let mut collector = SelectorDependencyCollector {
|
||||
map: self,
|
||||
document_state: &mut document_state,
|
||||
selector,
|
||||
parent_selectors: &mut parent_stack,
|
||||
quirks_mode,
|
||||
compound_state: PerCompoundState::new(0),
|
||||
alloc_error: &mut alloc_error,
|
||||
};
|
||||
|
||||
let visit_result = collector.visit_whole_selector();
|
||||
debug_assert_eq!(!visit_result, alloc_error.is_some());
|
||||
if let Some(alloc_error) = alloc_error {
|
||||
return Err(alloc_error);
|
||||
}
|
||||
}
|
||||
|
||||
if !document_state.is_empty() {
|
||||
let dep = DocumentStateDependency {
|
||||
state: document_state,
|
||||
selector: selector.clone(),
|
||||
};
|
||||
self.document_state_selectors.try_push(dep)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct PerCompoundState {
|
||||
/// The offset at which our compound starts.
|
||||
offset: usize,
|
||||
|
||||
/// The state this compound selector is affected by.
|
||||
element_state: ElementState,
|
||||
|
||||
/// Whether we've added a generic attribute dependency for this selector.
|
||||
added_attr_dependency: bool,
|
||||
}
|
||||
|
||||
impl PerCompoundState {
|
||||
fn new(offset: usize) -> Self {
|
||||
Self {
|
||||
offset,
|
||||
element_state: ElementState::empty(),
|
||||
added_attr_dependency: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct that collects invalidations for a given compound selector.
|
||||
struct SelectorDependencyCollector<'a> {
|
||||
map: &'a mut InvalidationMap,
|
||||
|
||||
/// The document this _complex_ selector is affected by.
|
||||
///
|
||||
/// We don't need to track state per compound selector, since it's global
|
||||
/// state and it changes for everything.
|
||||
document_state: &'a mut DocumentState,
|
||||
|
||||
/// The current selector and offset we're iterating.
|
||||
selector: &'a Selector<SelectorImpl>,
|
||||
|
||||
/// The stack of parent selectors that we have, and at which offset of the
|
||||
/// sequence.
|
||||
///
|
||||
/// This starts empty. It grows when we find nested :is and :where selector
|
||||
/// lists.
|
||||
parent_selectors: &'a mut SmallVec<[(Selector<SelectorImpl>, usize); 5]>,
|
||||
|
||||
/// The quirks mode of the document where we're inserting dependencies.
|
||||
quirks_mode: QuirksMode,
|
||||
|
||||
/// State relevant to a given compound selector.
|
||||
compound_state: PerCompoundState,
|
||||
|
||||
/// The allocation error, if we OOM.
|
||||
alloc_error: &'a mut Option<CollectionAllocErr>,
|
||||
}
|
||||
|
||||
impl<'a> SelectorDependencyCollector<'a> {
|
||||
fn visit_whole_selector(&mut self) -> bool {
|
||||
let mut iter = self.selector.iter();
|
||||
let mut combinator;
|
||||
let mut index = 0;
|
||||
|
||||
let mut document_state = DocumentState::empty();
|
||||
|
||||
loop {
|
||||
let sequence_start = index;
|
||||
|
||||
let mut compound_visitor = CompoundSelectorDependencyCollector {
|
||||
classes: SmallVec::new(),
|
||||
ids: SmallVec::new(),
|
||||
state: ElementState::empty(),
|
||||
document_state: &mut document_state,
|
||||
other_attributes: false,
|
||||
flags: &mut self.flags,
|
||||
};
|
||||
// Reset the compound state.
|
||||
self.compound_state = PerCompoundState::new(index);
|
||||
|
||||
// Visit all the simple selectors in this sequence.
|
||||
//
|
||||
// Note that this works because we can't have combinators nested
|
||||
// inside simple selectors (i.e. in :not() or :-moz-any()).
|
||||
//
|
||||
// If we ever support that we'll need to visit nested complex
|
||||
// selectors as well, in order to mark them as affecting descendants
|
||||
// at least.
|
||||
for ss in &mut iter {
|
||||
ss.visit(&mut compound_visitor);
|
||||
ss.visit(self);
|
||||
index += 1; // Account for the simple selector.
|
||||
}
|
||||
|
||||
for class in compound_visitor.classes {
|
||||
self.class_to_selector
|
||||
.try_entry(class, quirks_mode)?
|
||||
.or_insert_with(SmallVec::new)
|
||||
.try_push(Dependency {
|
||||
selector: selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
})?;
|
||||
}
|
||||
|
||||
for id in compound_visitor.ids {
|
||||
self.id_to_selector
|
||||
.try_entry(id, quirks_mode)?
|
||||
.or_insert_with(SmallVec::new)
|
||||
.try_push(Dependency {
|
||||
selector: selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
})?;
|
||||
}
|
||||
|
||||
if !compound_visitor.state.is_empty() {
|
||||
self.state_affecting_selectors.insert(
|
||||
if !self.compound_state.element_state.is_empty() {
|
||||
let dependency = self.dependency();
|
||||
let result = self.map.state_affecting_selectors.insert(
|
||||
StateDependency {
|
||||
dep: Dependency {
|
||||
selector: selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
},
|
||||
state: compound_visitor.state,
|
||||
dep: dependency,
|
||||
state: self.compound_state.element_state,
|
||||
},
|
||||
quirks_mode,
|
||||
)?;
|
||||
}
|
||||
|
||||
if compound_visitor.other_attributes {
|
||||
self.other_attribute_affecting_selectors.insert(
|
||||
Dependency {
|
||||
selector: selector.clone(),
|
||||
selector_offset: sequence_start,
|
||||
},
|
||||
quirks_mode,
|
||||
)?;
|
||||
self.quirks_mode,
|
||||
);
|
||||
if let Err(alloc_error) = result {
|
||||
*self.alloc_error = Some(alloc_error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
combinator = iter.next_sequence();
|
||||
|
@ -309,88 +368,107 @@ impl InvalidationMap {
|
|||
index += 1; // Account for the combinator.
|
||||
}
|
||||
|
||||
if !document_state.is_empty() {
|
||||
self.document_state_selectors
|
||||
.try_push(DocumentStateDependency {
|
||||
state: document_state,
|
||||
selector: selector.clone(),
|
||||
})?;
|
||||
true
|
||||
}
|
||||
|
||||
fn add_attr_dependency(&mut self) -> bool {
|
||||
debug_assert!(!self.compound_state.added_attr_dependency);
|
||||
self.compound_state.added_attr_dependency = true;
|
||||
|
||||
let dependency = self.dependency();
|
||||
let result = self.map.other_attribute_affecting_selectors.insert(
|
||||
dependency,
|
||||
self.quirks_mode,
|
||||
);
|
||||
if let Err(alloc_error) = result {
|
||||
*self.alloc_error = Some(alloc_error);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn dependency(&self) -> Dependency {
|
||||
let mut parent = None;
|
||||
|
||||
// TODO(emilio): Maybe we should refcount the parent dependencies, or
|
||||
// cache them or something.
|
||||
for &(ref selector, ref selector_offset) in self.parent_selectors.iter() {
|
||||
let new_parent = Dependency {
|
||||
selector: selector.clone(),
|
||||
selector_offset: *selector_offset,
|
||||
parent,
|
||||
};
|
||||
parent = Some(Box::new(new_parent));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Dependency {
|
||||
selector: self.selector.clone(),
|
||||
selector_offset: self.compound_state.offset,
|
||||
parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct that collects invalidations for a given compound selector.
|
||||
///
|
||||
/// FIXME(emilio, bug 1509418): :where is mishandled, figure out the right way
|
||||
/// to do invalidation for :where when combinators are inside.
|
||||
///
|
||||
/// Simplest feasible idea seems to be: For each :where branch, if there are
|
||||
/// combinators in the branch, treat as its own selector (basically, call
|
||||
/// `.note_selector` with the nested selector). That may over-invalidate, but
|
||||
/// not too much. If there are no combinators, then behave like we do today and
|
||||
/// use the outer selector as a whole. If we care a lot about that potential
|
||||
/// over-invalidation where combinators are present then we need more complex
|
||||
/// data-structures in `Dependency`.
|
||||
struct CompoundSelectorDependencyCollector<'a> {
|
||||
/// The state this compound selector is affected by.
|
||||
state: ElementState,
|
||||
|
||||
/// The document this _complex_ selector is affected by.
|
||||
///
|
||||
/// We don't need to track state per compound selector, since it's global
|
||||
/// state and it changes for everything.
|
||||
document_state: &'a mut DocumentState,
|
||||
|
||||
/// The classes this compound selector is affected by.
|
||||
///
|
||||
/// NB: This will be often a single class, but could be multiple in
|
||||
/// presence of :not, :-moz-any, .foo.bar.baz, etc.
|
||||
classes: SmallVec<[Atom; 5]>,
|
||||
|
||||
/// The IDs this compound selector is affected by.
|
||||
///
|
||||
/// NB: This will be almost always a single id, but could be multiple in
|
||||
/// presence of :not, :-moz-any, #foo#bar, etc.
|
||||
ids: SmallVec<[Atom; 5]>,
|
||||
|
||||
/// Whether it affects other attribute-dependent selectors that aren't ID or
|
||||
/// class selectors (NB: We still set this to true in presence of [class] or
|
||||
/// [id] attribute selectors).
|
||||
other_attributes: bool,
|
||||
|
||||
/// The invalidation map flags, that we set when some attribute selectors are present.
|
||||
flags: &'a mut InvalidationMapFlags,
|
||||
}
|
||||
|
||||
impl<'a> SelectorVisitor for CompoundSelectorDependencyCollector<'a> {
|
||||
impl<'a> SelectorVisitor for SelectorDependencyCollector<'a> {
|
||||
type Impl = SelectorImpl;
|
||||
|
||||
fn visit_selector_list(&mut self, list: &[Selector<SelectorImpl>]) -> bool {
|
||||
self.parent_selectors.push((self.selector.clone(), self.compound_state.offset));
|
||||
for selector in list {
|
||||
let mut nested = SelectorDependencyCollector {
|
||||
map: &mut *self.map,
|
||||
document_state: &mut *self.document_state,
|
||||
selector,
|
||||
parent_selectors: &mut *self.parent_selectors,
|
||||
quirks_mode: self.quirks_mode,
|
||||
compound_state: PerCompoundState::new(0),
|
||||
alloc_error: &mut *self.alloc_error,
|
||||
};
|
||||
if !nested.visit_whole_selector() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.parent_selectors.pop();
|
||||
true
|
||||
}
|
||||
|
||||
fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
|
||||
#[cfg(feature = "gecko")]
|
||||
use crate::selector_parser::NonTSPseudoClass;
|
||||
|
||||
match *s {
|
||||
Component::ID(ref id) => {
|
||||
self.ids.push(id.clone());
|
||||
},
|
||||
Component::Class(ref class) => {
|
||||
self.classes.push(class.clone());
|
||||
Component::ID(ref atom) | Component::Class(ref atom) => {
|
||||
let dependency = self.dependency();
|
||||
let map = match *s {
|
||||
Component::ID(..) => &mut self.map.id_to_selector,
|
||||
Component::Class(..) => &mut self.map.class_to_selector,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let entry = match map.try_entry(atom.clone(), self.quirks_mode) {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
*self.alloc_error = Some(err);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
entry.or_insert_with(SmallVec::new).try_push(dependency).is_ok()
|
||||
},
|
||||
Component::NonTSPseudoClass(ref pc) => {
|
||||
self.other_attributes |= pc.is_attr_based();
|
||||
self.state |= match *pc {
|
||||
self.compound_state.element_state |= match *pc {
|
||||
#[cfg(feature = "gecko")]
|
||||
NonTSPseudoClass::Dir(ref dir) => dir.element_state(),
|
||||
_ => pc.state_flag(),
|
||||
};
|
||||
*self.document_state |= pc.document_state_flag();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
true
|
||||
if !self.compound_state.added_attr_dependency && pc.is_attr_based() {
|
||||
self.add_attr_dependency()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_attribute_selector(
|
||||
|
@ -399,7 +477,6 @@ impl<'a> SelectorVisitor for CompoundSelectorDependencyCollector<'a> {
|
|||
_local_name: &LocalName,
|
||||
local_name_lower: &LocalName,
|
||||
) -> bool {
|
||||
self.other_attributes = true;
|
||||
let may_match_in_no_namespace = match *constraint {
|
||||
NamespaceConstraint::Any => true,
|
||||
NamespaceConstraint::Specific(ref ns) => ns.is_empty(),
|
||||
|
@ -407,14 +484,16 @@ impl<'a> SelectorVisitor for CompoundSelectorDependencyCollector<'a> {
|
|||
|
||||
if may_match_in_no_namespace {
|
||||
if *local_name_lower == local_name!("id") {
|
||||
self.flags
|
||||
.insert(InvalidationMapFlags::HAS_ID_ATTR_SELECTOR)
|
||||
self.map.flags.insert(InvalidationMapFlags::HAS_ID_ATTR_SELECTOR)
|
||||
} else if *local_name_lower == local_name!("class") {
|
||||
self.flags
|
||||
.insert(InvalidationMapFlags::HAS_CLASS_ATTR_SELECTOR)
|
||||
self.map.flags.insert(InvalidationMapFlags::HAS_CLASS_ATTR_SELECTOR)
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
if !self.compound_state.added_attr_dependency {
|
||||
self.add_attr_dependency()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,39 @@ impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E>
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks a dependency against a given element and wrapper, to see if something
|
||||
/// changed.
|
||||
pub fn check_dependency<E, W>(
|
||||
dependency: &Dependency,
|
||||
element: &E,
|
||||
wrapper: &W,
|
||||
context: &mut MatchingContext<'_, SelectorImpl>,
|
||||
)
|
||||
where
|
||||
E: TElement,
|
||||
W: selectors::Element<Impl = E::Impl>,
|
||||
{
|
||||
let matches_now = matches_selector(
|
||||
&dependency.selector,
|
||||
dependency.selector_offset,
|
||||
None,
|
||||
element,
|
||||
&mut context,
|
||||
&mut |_, _| {},
|
||||
);
|
||||
|
||||
let matched_then = matches_selector(
|
||||
&dependency.selector,
|
||||
dependency.selector_offset,
|
||||
None,
|
||||
wrapper,
|
||||
&mut context,
|
||||
&mut |_, _| {},
|
||||
);
|
||||
|
||||
matched_then != matches_now
|
||||
}
|
||||
|
||||
/// Whether we should process the descendants of a given element for style
|
||||
/// invalidation.
|
||||
pub fn should_process_descendants(data: &ElementData) -> bool {
|
||||
|
@ -412,28 +445,9 @@ where
|
|||
}
|
||||
|
||||
/// Check whether a dependency should be taken into account.
|
||||
#[inline]
|
||||
fn check_dependency(&mut self, dependency: &Dependency) -> bool {
|
||||
let element = &self.element;
|
||||
let wrapper = &self.wrapper;
|
||||
let matches_now = matches_selector(
|
||||
&dependency.selector,
|
||||
dependency.selector_offset,
|
||||
None,
|
||||
element,
|
||||
&mut self.matching_context,
|
||||
&mut |_, _| {},
|
||||
);
|
||||
|
||||
let matched_then = matches_selector(
|
||||
&dependency.selector,
|
||||
dependency.selector_offset,
|
||||
None,
|
||||
wrapper,
|
||||
&mut self.matching_context,
|
||||
&mut |_, _| {},
|
||||
);
|
||||
|
||||
matched_then != matches_now
|
||||
check_dependency(&self.element, &self.wrapper, &mut self.matching_context)
|
||||
}
|
||||
|
||||
fn scan_dependency(&mut self, dependency: &'selectors Dependency) {
|
||||
|
@ -456,7 +470,13 @@ where
|
|||
|
||||
let invalidation_kind = dependency.invalidation_kind();
|
||||
if matches!(invalidation_kind, DependencyInvalidationKind::Element) {
|
||||
self.invalidates_self = true;
|
||||
if let Some(ref parent) = dependency.parent {
|
||||
// We know something changed in the inner selector, go outwards
|
||||
// now.
|
||||
self.scan_dependency(parent);
|
||||
} else {
|
||||
self.invalidates_self = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue