1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4 
5 //! A wrapper over an element and a snapshot, that allows us to selector-match
6 //! against a past state of the element.
7 
8 use crate::dom::TElement;
9 use crate::element_state::ElementState;
10 use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl};
11 use crate::selector_parser::{Snapshot, SnapshotMap};
12 use crate::values::AtomIdent;
13 use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom};
14 use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
15 use selectors::matching::{ElementSelectorFlags, MatchingContext};
16 use selectors::{Element, OpaqueElement};
17 use std::cell::Cell;
18 use std::fmt;
19 
20 /// In order to compute restyle hints, we perform a selector match against a
21 /// list of partial selectors whose rightmost simple selector may be sensitive
22 /// to the thing being changed. We do this matching twice, once for the element
23 /// as it exists now and once for the element as it existed at the time of the
24 /// last restyle. If the results of the selector match differ, that means that
25 /// the given partial selector is sensitive to the change, and we compute a
26 /// restyle hint based on its combinator.
27 ///
28 /// In order to run selector matching against the old element state, we generate
29 /// a wrapper for the element which claims to have the old state. This is the
30 /// ElementWrapper logic below.
31 ///
32 /// Gecko does this differently for element states, and passes a mask called
33 /// mStateMask, which indicates the states that need to be ignored during
34 /// selector matching. This saves an ElementWrapper allocation and an additional
35 /// selector match call at the expense of additional complexity inside the
36 /// selector matching logic. This only works for boolean states though, so we
37 /// still need to take the ElementWrapper approach for attribute-dependent
38 /// style. So we do it the same both ways for now to reduce complexity, but it's
39 /// worth measuring the performance impact (if any) of the mStateMask approach.
40 pub trait ElementSnapshot: Sized {
41     /// The state of the snapshot, if any.
state(&self) -> Option<ElementState>42     fn state(&self) -> Option<ElementState>;
43 
44     /// If this snapshot contains attribute information.
has_attrs(&self) -> bool45     fn has_attrs(&self) -> bool;
46 
47     /// Gets the attribute information of the snapshot as a string.
48     ///
49     /// Only for debugging purposes.
debug_list_attributes(&self) -> String50     fn debug_list_attributes(&self) -> String {
51         String::new()
52     }
53 
54     /// The ID attribute per this snapshot. Should only be called if
55     /// `has_attrs()` returns true.
id_attr(&self) -> Option<&WeakAtom>56     fn id_attr(&self) -> Option<&WeakAtom>;
57 
58     /// Whether this snapshot contains the class `name`. Should only be called
59     /// if `has_attrs()` returns true.
has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool60     fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool;
61 
62     /// Whether this snapshot represents the part named `name`. Should only be
63     /// called if `has_attrs()` returns true.
is_part(&self, name: &AtomIdent) -> bool64     fn is_part(&self, name: &AtomIdent) -> bool;
65 
66     /// See Element::imported_part.
imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>67     fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>;
68 
69     /// A callback that should be called for each class of the snapshot. Should
70     /// only be called if `has_attrs()` returns true.
each_class<F>(&self, _: F) where F: FnMut(&AtomIdent)71     fn each_class<F>(&self, _: F)
72     where
73         F: FnMut(&AtomIdent);
74 
75     /// The `xml:lang=""` or `lang=""` attribute value per this snapshot.
lang_attr(&self) -> Option<AttrValue>76     fn lang_attr(&self) -> Option<AttrValue>;
77 }
78 
79 /// A simple wrapper over an element and a snapshot, that allows us to
80 /// selector-match against a past state of the element.
81 #[derive(Clone)]
82 pub struct ElementWrapper<'a, E>
83 where
84     E: TElement,
85 {
86     element: E,
87     cached_snapshot: Cell<Option<&'a Snapshot>>,
88     snapshot_map: &'a SnapshotMap,
89 }
90 
91 impl<'a, E> ElementWrapper<'a, E>
92 where
93     E: TElement,
94 {
95     /// Trivially constructs an `ElementWrapper`.
new(el: E, snapshot_map: &'a SnapshotMap) -> Self96     pub fn new(el: E, snapshot_map: &'a SnapshotMap) -> Self {
97         ElementWrapper {
98             element: el,
99             cached_snapshot: Cell::new(None),
100             snapshot_map: snapshot_map,
101         }
102     }
103 
104     /// Gets the snapshot associated with this element, if any.
snapshot(&self) -> Option<&'a Snapshot>105     pub fn snapshot(&self) -> Option<&'a Snapshot> {
106         if !self.element.has_snapshot() {
107             return None;
108         }
109 
110         if let Some(s) = self.cached_snapshot.get() {
111             return Some(s);
112         }
113 
114         let snapshot = self.snapshot_map.get(&self.element);
115         debug_assert!(snapshot.is_some(), "has_snapshot lied!");
116 
117         self.cached_snapshot.set(snapshot);
118 
119         snapshot
120     }
121 
122     /// Returns the states that have changed since the element was snapshotted.
state_changes(&self) -> ElementState123     pub fn state_changes(&self) -> ElementState {
124         let snapshot = match self.snapshot() {
125             Some(s) => s,
126             None => return ElementState::empty(),
127         };
128 
129         match snapshot.state() {
130             Some(state) => state ^ self.element.state(),
131             None => ElementState::empty(),
132         }
133     }
134 
135     /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`)
136     /// attribute from this element's snapshot or the closest ancestor
137     /// element snapshot with the attribute specified.
get_lang(&self) -> Option<AttrValue>138     fn get_lang(&self) -> Option<AttrValue> {
139         let mut current = self.clone();
140         loop {
141             let lang = match self.snapshot() {
142                 Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(),
143                 _ => current.element.lang_attr(),
144             };
145             if lang.is_some() {
146                 return lang;
147             }
148             current = current.parent_element()?;
149         }
150     }
151 }
152 
153 impl<'a, E> fmt::Debug for ElementWrapper<'a, E>
154 where
155     E: TElement,
156 {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result157     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158         // Ignore other fields for now, can change later if needed.
159         self.element.fmt(f)
160     }
161 }
162 
163 impl<'a, E> Element for ElementWrapper<'a, E>
164 where
165     E: TElement,
166 {
167     type Impl = SelectorImpl;
168 
match_non_ts_pseudo_class<F>( &self, pseudo_class: &NonTSPseudoClass, context: &mut MatchingContext<Self::Impl>, _setter: &mut F, ) -> bool where F: FnMut(&Self, ElementSelectorFlags),169     fn match_non_ts_pseudo_class<F>(
170         &self,
171         pseudo_class: &NonTSPseudoClass,
172         context: &mut MatchingContext<Self::Impl>,
173         _setter: &mut F,
174     ) -> bool
175     where
176         F: FnMut(&Self, ElementSelectorFlags),
177     {
178         // Some pseudo-classes need special handling to evaluate them against
179         // the snapshot.
180         match *pseudo_class {
181             // :dir is implemented in terms of state flags, but which state flag
182             // it maps to depends on the argument to :dir.  That means we can't
183             // just add its state flags to the NonTSPseudoClass, because if we
184             // added all of them there, and tested via intersects() here, we'd
185             // get incorrect behavior for :not(:dir()) cases.
186             //
187             // FIXME(bz): How can I set this up so once Servo adds :dir()
188             // support we don't forget to update this code?
189             #[cfg(feature = "gecko")]
190             NonTSPseudoClass::Dir(ref dir) => {
191                 let selector_flag = dir.element_state();
192                 if selector_flag.is_empty() {
193                     // :dir() with some random argument; does not match.
194                     return false;
195                 }
196                 let state = match self.snapshot().and_then(|s| s.state()) {
197                     Some(snapshot_state) => snapshot_state,
198                     None => self.element.state(),
199                 };
200                 return state.contains(selector_flag);
201             },
202 
203             // For :link and :visited, we don't actually want to test the
204             // element state directly.
205             //
206             // Instead, we use the `visited_handling` to determine if they
207             // match.
208             NonTSPseudoClass::Link => {
209                 return self.is_link() && context.visited_handling().matches_unvisited();
210             },
211             NonTSPseudoClass::Visited => {
212                 return self.is_link() && context.visited_handling().matches_visited();
213             },
214 
215             #[cfg(feature = "gecko")]
216             NonTSPseudoClass::MozTableBorderNonzero => {
217                 if let Some(snapshot) = self.snapshot() {
218                     if snapshot.has_other_pseudo_class_state() {
219                         return snapshot.mIsTableBorderNonzero();
220                     }
221                 }
222             },
223 
224             #[cfg(feature = "gecko")]
225             NonTSPseudoClass::MozBrowserFrame => {
226                 if let Some(snapshot) = self.snapshot() {
227                     if snapshot.has_other_pseudo_class_state() {
228                         return snapshot.mIsMozBrowserFrame();
229                     }
230                 }
231             },
232 
233             #[cfg(feature = "gecko")]
234             NonTSPseudoClass::MozSelectListBox => {
235                 if let Some(snapshot) = self.snapshot() {
236                     if snapshot.has_other_pseudo_class_state() {
237                         return snapshot.mIsSelectListBox();
238                     }
239                 }
240             },
241 
242             // :lang() needs to match using the closest ancestor xml:lang="" or
243             // lang="" attribtue from snapshots.
244             NonTSPseudoClass::Lang(ref lang_arg) => {
245                 return self
246                     .element
247                     .match_element_lang(Some(self.get_lang()), lang_arg);
248             },
249 
250             _ => {},
251         }
252 
253         let flag = pseudo_class.state_flag();
254         if flag.is_empty() {
255             return self
256                 .element
257                 .match_non_ts_pseudo_class(pseudo_class, context, &mut |_, _| {});
258         }
259         match self.snapshot().and_then(|s| s.state()) {
260             Some(snapshot_state) => snapshot_state.intersects(flag),
261             None => self
262                 .element
263                 .match_non_ts_pseudo_class(pseudo_class, context, &mut |_, _| {}),
264         }
265     }
266 
match_pseudo_element( &self, pseudo_element: &PseudoElement, context: &mut MatchingContext<Self::Impl>, ) -> bool267     fn match_pseudo_element(
268         &self,
269         pseudo_element: &PseudoElement,
270         context: &mut MatchingContext<Self::Impl>,
271     ) -> bool {
272         self.element.match_pseudo_element(pseudo_element, context)
273     }
274 
is_link(&self) -> bool275     fn is_link(&self) -> bool {
276         match self.snapshot().and_then(|s| s.state()) {
277             Some(state) => state.intersects(ElementState::IN_VISITED_OR_UNVISITED_STATE),
278             None => self.element.is_link(),
279         }
280     }
281 
opaque(&self) -> OpaqueElement282     fn opaque(&self) -> OpaqueElement {
283         self.element.opaque()
284     }
285 
parent_element(&self) -> Option<Self>286     fn parent_element(&self) -> Option<Self> {
287         let parent = self.element.parent_element()?;
288         Some(Self::new(parent, self.snapshot_map))
289     }
290 
parent_node_is_shadow_root(&self) -> bool291     fn parent_node_is_shadow_root(&self) -> bool {
292         self.element.parent_node_is_shadow_root()
293     }
294 
containing_shadow_host(&self) -> Option<Self>295     fn containing_shadow_host(&self) -> Option<Self> {
296         let host = self.element.containing_shadow_host()?;
297         Some(Self::new(host, self.snapshot_map))
298     }
299 
prev_sibling_element(&self) -> Option<Self>300     fn prev_sibling_element(&self) -> Option<Self> {
301         let sibling = self.element.prev_sibling_element()?;
302         Some(Self::new(sibling, self.snapshot_map))
303     }
304 
next_sibling_element(&self) -> Option<Self>305     fn next_sibling_element(&self) -> Option<Self> {
306         let sibling = self.element.next_sibling_element()?;
307         Some(Self::new(sibling, self.snapshot_map))
308     }
309 
310     #[inline]
is_html_element_in_html_document(&self) -> bool311     fn is_html_element_in_html_document(&self) -> bool {
312         self.element.is_html_element_in_html_document()
313     }
314 
315     #[inline]
is_html_slot_element(&self) -> bool316     fn is_html_slot_element(&self) -> bool {
317         self.element.is_html_slot_element()
318     }
319 
320     #[inline]
has_local_name( &self, local_name: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName, ) -> bool321     fn has_local_name(
322         &self,
323         local_name: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName,
324     ) -> bool {
325         self.element.has_local_name(local_name)
326     }
327 
328     #[inline]
has_namespace( &self, ns: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl, ) -> bool329     fn has_namespace(
330         &self,
331         ns: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl,
332     ) -> bool {
333         self.element.has_namespace(ns)
334     }
335 
336     #[inline]
is_same_type(&self, other: &Self) -> bool337     fn is_same_type(&self, other: &Self) -> bool {
338         self.element.is_same_type(&other.element)
339     }
340 
attr_matches( &self, ns: &NamespaceConstraint<&Namespace>, local_name: &LocalName, operation: &AttrSelectorOperation<&AttrValue>, ) -> bool341     fn attr_matches(
342         &self,
343         ns: &NamespaceConstraint<&Namespace>,
344         local_name: &LocalName,
345         operation: &AttrSelectorOperation<&AttrValue>,
346     ) -> bool {
347         match self.snapshot() {
348             Some(snapshot) if snapshot.has_attrs() => {
349                 snapshot.attr_matches(ns, local_name, operation)
350             },
351             _ => self.element.attr_matches(ns, local_name, operation),
352         }
353     }
354 
has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool355     fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
356         match self.snapshot() {
357             Some(snapshot) if snapshot.has_attrs() => snapshot
358                 .id_attr()
359                 .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)),
360             _ => self.element.has_id(id, case_sensitivity),
361         }
362     }
363 
is_part(&self, name: &AtomIdent) -> bool364     fn is_part(&self, name: &AtomIdent) -> bool {
365         match self.snapshot() {
366             Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name),
367             _ => self.element.is_part(name),
368         }
369     }
370 
imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>371     fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> {
372         match self.snapshot() {
373             Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name),
374             _ => self.element.imported_part(name),
375         }
376     }
377 
has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool378     fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
379         match self.snapshot() {
380             Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity),
381             _ => self.element.has_class(name, case_sensitivity),
382         }
383     }
384 
is_empty(&self) -> bool385     fn is_empty(&self) -> bool {
386         self.element.is_empty()
387     }
388 
is_root(&self) -> bool389     fn is_root(&self) -> bool {
390         self.element.is_root()
391     }
392 
is_pseudo_element(&self) -> bool393     fn is_pseudo_element(&self) -> bool {
394         self.element.is_pseudo_element()
395     }
396 
pseudo_element_originating_element(&self) -> Option<Self>397     fn pseudo_element_originating_element(&self) -> Option<Self> {
398         self.element
399             .pseudo_element_originating_element()
400             .map(|e| ElementWrapper::new(e, self.snapshot_map))
401     }
402 
assigned_slot(&self) -> Option<Self>403     fn assigned_slot(&self) -> Option<Self> {
404         self.element
405             .assigned_slot()
406             .map(|e| ElementWrapper::new(e, self.snapshot_map))
407     }
408 }
409