script/dom/
documentorshadowroot.rs

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
5use std::collections::HashSet;
6use std::ffi::c_void;
7use std::fmt;
8
9use embedder_traits::UntrustedNodeAddress;
10use js::rust::HandleValue;
11use layout_api::ElementsFromPointFlags;
12use rustc_hash::FxBuildHasher;
13use script_bindings::error::{Error, ErrorResult};
14use script_bindings::script_runtime::JSContext;
15use servo_arc::Arc;
16use servo_config::pref;
17use style::invalidation::media_queries::{MediaListKey, ToMediaListKey};
18use style::media_queries::MediaList;
19use style::shared_lock::{SharedRwLock as StyleSharedRwLock, SharedRwLockReadGuard};
20use style::stylesheets::scope_rule::ImplicitScopeRoot;
21use style::stylesheets::{Stylesheet, StylesheetContents};
22use stylo_atoms::Atom;
23use webrender_api::units::LayoutPoint;
24
25use super::bindings::trace::HashMapTracedValues;
26use crate::dom::bindings::cell::DomRefCell;
27use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
28use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods;
29use crate::dom::bindings::conversions::{ConversionResult, SafeFromJSValConvertible};
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::num::Finite;
32use crate::dom::bindings::root::{Dom, DomRoot};
33use crate::dom::element::Element;
34use crate::dom::html::htmlelement::HTMLElement;
35use crate::dom::node::{self, Node, VecPreOrderInsertionHelper};
36use crate::dom::shadowroot::ShadowRoot;
37use crate::dom::stylesheetlist::StyleSheetListOwner;
38use crate::dom::types::CSSStyleSheet;
39use crate::dom::window::Window;
40use crate::stylesheet_set::StylesheetSetRef;
41
42/// Stylesheet could be constructed by a CSSOM object CSSStylesheet or parsed
43/// from HTML element such as `<style>` or `<link>`.
44#[derive(Clone, JSTraceable, MallocSizeOf)]
45#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
46pub(crate) enum StylesheetSource {
47    Element(Dom<Element>),
48    Constructed(Dom<CSSStyleSheet>),
49}
50
51impl StylesheetSource {
52    pub(crate) fn get_cssom_object(&self) -> Option<DomRoot<CSSStyleSheet>> {
53        match self {
54            StylesheetSource::Element(el) => el.upcast::<Node>().get_cssom_stylesheet(),
55            StylesheetSource::Constructed(ss) => Some(ss.as_rooted()),
56        }
57    }
58
59    pub(crate) fn is_a_valid_owner(&self) -> bool {
60        match self {
61            StylesheetSource::Element(el) => el.as_stylesheet_owner().is_some(),
62            StylesheetSource::Constructed(ss) => ss.is_constructed(),
63        }
64    }
65
66    pub(crate) fn is_constructed(&self) -> bool {
67        matches!(self, StylesheetSource::Constructed(_))
68    }
69}
70
71#[derive(Clone, JSTraceable, MallocSizeOf)]
72#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
73pub(crate) struct ServoStylesheetInDocument {
74    #[ignore_malloc_size_of = "Arc"]
75    #[no_trace]
76    pub(crate) sheet: Arc<Stylesheet>,
77    /// The object that owns this stylesheet. For constructed stylesheet, it would be the
78    /// CSSOM object itself, and for stylesheet generated by an element, it would be the
79    /// html element. This is used to get the CSSOM Stylesheet within a DocumentOrShadowDOM.
80    pub(crate) owner: StylesheetSource,
81}
82
83// This is necessary because this type is contained within a Stylo type which needs
84// Stylo's version of MallocSizeOf.
85impl stylo_malloc_size_of::MallocSizeOf for ServoStylesheetInDocument {
86    fn size_of(&self, ops: &mut stylo_malloc_size_of::MallocSizeOfOps) -> usize {
87        <ServoStylesheetInDocument as malloc_size_of::MallocSizeOf>::size_of(self, ops)
88    }
89}
90
91impl fmt::Debug for ServoStylesheetInDocument {
92    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
93        self.sheet.fmt(formatter)
94    }
95}
96
97impl PartialEq for ServoStylesheetInDocument {
98    fn eq(&self, other: &Self) -> bool {
99        Arc::ptr_eq(&self.sheet, &other.sheet)
100    }
101}
102
103impl ToMediaListKey for ServoStylesheetInDocument {
104    fn to_media_list_key(&self) -> MediaListKey {
105        self.sheet.contents.to_media_list_key()
106    }
107}
108
109impl ::style::stylesheets::StylesheetInDocument for ServoStylesheetInDocument {
110    fn enabled(&self) -> bool {
111        self.sheet.enabled()
112    }
113
114    fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
115        self.sheet.media(guard)
116    }
117
118    fn contents(&self) -> &StylesheetContents {
119        self.sheet.contents()
120    }
121
122    fn implicit_scope_root(&self) -> Option<ImplicitScopeRoot> {
123        None
124    }
125}
126
127// https://w3c.github.io/webcomponents/spec/shadow/#extensions-to-the-documentorshadowroot-mixin
128#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
129#[derive(Clone, JSTraceable, MallocSizeOf)]
130pub(crate) struct DocumentOrShadowRoot {
131    window: Dom<Window>,
132}
133
134impl DocumentOrShadowRoot {
135    pub(crate) fn new(window: &Window) -> Self {
136        Self {
137            window: Dom::from_ref(window),
138        }
139    }
140
141    #[allow(unsafe_code)]
142    // https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
143    pub(crate) fn element_from_point(
144        &self,
145        x: Finite<f64>,
146        y: Finite<f64>,
147        document_element: Option<DomRoot<Element>>,
148        has_browsing_context: bool,
149    ) -> Option<DomRoot<Element>> {
150        let x = *x as f32;
151        let y = *y as f32;
152        let viewport = self.window.viewport_details().size;
153
154        if !has_browsing_context {
155            return None;
156        }
157
158        if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
159            return None;
160        }
161
162        match self
163            .window
164            .elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::empty())
165            .first()
166        {
167            Some(result) => {
168                // SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
169                // layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
170                let address = UntrustedNodeAddress(result.node.0 as *const c_void);
171                let node = unsafe { node::from_untrusted_node_address(address) };
172                let parent_node = node.GetParentNode().unwrap();
173                let shadow_host = parent_node
174                    .downcast::<ShadowRoot>()
175                    .map(ShadowRootMethods::Host);
176                let element_ref = node
177                    .downcast::<Element>()
178                    .or(shadow_host.as_deref())
179                    .unwrap_or_else(|| {
180                        parent_node
181                            .downcast::<Element>()
182                            .expect("Hit node should have an element or shadowroot parent")
183                    });
184
185                Some(DomRoot::from_ref(element_ref))
186            },
187            None => document_element,
188        }
189    }
190
191    #[allow(unsafe_code)]
192    // https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
193    pub(crate) fn elements_from_point(
194        &self,
195        x: Finite<f64>,
196        y: Finite<f64>,
197        document_element: Option<DomRoot<Element>>,
198        has_browsing_context: bool,
199    ) -> Vec<DomRoot<Element>> {
200        let x = *x as f32;
201        let y = *y as f32;
202        let viewport = self.window.viewport_details().size;
203
204        if !has_browsing_context {
205            return vec![];
206        }
207
208        // Step 2
209        if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
210            return vec![];
211        }
212
213        // Step 1 and Step 3
214        let nodes = self
215            .window
216            .elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::FindAll);
217        let mut elements: Vec<DomRoot<Element>> = nodes
218            .iter()
219            .flat_map(|result| {
220                // SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
221                // layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
222                let address = UntrustedNodeAddress(result.node.0 as *const c_void);
223                let node = unsafe { node::from_untrusted_node_address(address) };
224                DomRoot::downcast::<Element>(node)
225            })
226            .collect();
227
228        // Step 4
229        if let Some(root_element) = document_element {
230            if elements.last() != Some(&root_element) {
231                elements.push(root_element);
232            }
233        }
234
235        // Step 5
236        elements
237    }
238
239    // https://html.spec.whatwg.org/multipage/#dom-document-activeelement
240    pub(crate) fn get_active_element(
241        &self,
242        focused_element: Option<DomRoot<Element>>,
243        body: Option<DomRoot<HTMLElement>>,
244        document_element: Option<DomRoot<Element>>,
245    ) -> Option<DomRoot<Element>> {
246        // TODO: Step 2.
247
248        match focused_element {
249            Some(element) => Some(element), // Step 3. and 4.
250            None => match body {
251                // Step 5.
252                Some(body) => Some(DomRoot::upcast(body)),
253                None => document_element,
254            },
255        }
256    }
257
258    /// Remove a stylesheet owned by `owner` from the list of document sheets.
259    #[cfg_attr(crown, allow(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
260    pub(crate) fn remove_stylesheet(
261        owner: StylesheetSource,
262        s: &Arc<Stylesheet>,
263        mut stylesheets: StylesheetSetRef<ServoStylesheetInDocument>,
264    ) {
265        let guard = s.shared_lock.read();
266
267        // FIXME(emilio): Would be nice to remove the clone, etc.
268        stylesheets.remove_stylesheet(
269            None,
270            ServoStylesheetInDocument {
271                sheet: s.clone(),
272                owner,
273            },
274            &guard,
275        );
276    }
277
278    /// Add a stylesheet owned by `owner` to the list of document sheets, in the
279    /// correct tree position.
280    #[cfg_attr(crown, allow(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
281    pub(crate) fn add_stylesheet(
282        owner: StylesheetSource,
283        mut stylesheets: StylesheetSetRef<ServoStylesheetInDocument>,
284        sheet: Arc<Stylesheet>,
285        insertion_point: Option<ServoStylesheetInDocument>,
286        style_shared_lock: &StyleSharedRwLock,
287    ) {
288        debug_assert!(owner.is_a_valid_owner(), "Wat");
289
290        if owner.is_constructed() && !pref!(dom_adoptedstylesheet_enabled) {
291            return;
292        }
293
294        let sheet = ServoStylesheetInDocument { sheet, owner };
295
296        let guard = style_shared_lock.read();
297
298        match insertion_point {
299            Some(ip) => {
300                stylesheets.insert_stylesheet_before(None, sheet, ip, &guard);
301            },
302            None => {
303                stylesheets.append_stylesheet(None, sheet, &guard);
304            },
305        }
306    }
307
308    /// Remove any existing association between the provided id/name and any elements in this document.
309    pub(crate) fn unregister_named_element(
310        &self,
311        id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
312        to_unregister: &Element,
313        id: &Atom,
314    ) {
315        debug!(
316            "Removing named element {:p}: {:p} id={}",
317            self, to_unregister, id
318        );
319        let mut id_map = id_map.borrow_mut();
320        let is_empty = match id_map.get_mut(id) {
321            None => false,
322            Some(elements) => {
323                let position = elements
324                    .iter()
325                    .position(|element| &**element == to_unregister)
326                    .expect("This element should be in registered.");
327                elements.remove(position);
328                elements.is_empty()
329            },
330        };
331        if is_empty {
332            id_map.remove(id);
333        }
334    }
335
336    /// Associate an element present in this document with the provided id/name.
337    pub(crate) fn register_named_element(
338        &self,
339        id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
340        element: &Element,
341        id: &Atom,
342        root: DomRoot<Node>,
343    ) {
344        debug!("Adding named element {:p}: {:p} id={}", self, element, id);
345        assert!(
346            element.upcast::<Node>().is_in_a_document_tree() ||
347                element.upcast::<Node>().is_in_a_shadow_tree()
348        );
349        assert!(!id.is_empty());
350        let mut id_map = id_map.borrow_mut();
351        let elements = id_map.entry(id.clone()).or_default();
352        elements.insert_pre_order(element, &root);
353    }
354
355    /// Inner part of adopted stylesheet. We are setting it by, assuming it is a FrozenArray
356    /// instead of an ObservableArray. Thus, it would have a completely different workflow
357    /// compared to the spec. The workflow here is actually following Gecko's implementation
358    /// of AdoptedStylesheet before the implementation of ObservableArray.
359    ///
360    /// The main purpose from this function is to set the `&mut adopted_stylesheet` to match
361    /// `incoming_stylesheet` and update the corresponding Styleset in a Document or a ShadowRoot.
362    /// In case of duplicates, the setter will respect the last duplicates.
363    ///
364    /// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets>
365    // TODO: Handle duplicated adoptedstylesheet correctly, Stylo is preventing duplicates inside a
366    //       Stylesheet Set. But this is not ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1978755
367    fn set_adopted_stylesheet(
368        adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
369        incoming_stylesheets: &[Dom<CSSStyleSheet>],
370        owner: &StyleSheetListOwner,
371    ) -> ErrorResult {
372        if !pref!(dom_adoptedstylesheet_enabled) {
373            return Ok(());
374        }
375
376        let owner_doc = match owner {
377            StyleSheetListOwner::Document(doc) => doc,
378            StyleSheetListOwner::ShadowRoot(root) => root.owner_doc(),
379        };
380
381        for sheet in incoming_stylesheets.iter() {
382            // > If value’s constructed flag is not set, or its constructor document is not equal
383            // > to this DocumentOrShadowRoot’s node document, throw a "NotAllowedError" DOMException.
384            if !sheet.constructor_document_matches(owner_doc) {
385                return Err(Error::NotAllowed);
386            }
387        }
388
389        // The set to check for the duplicates when removing the old stylesheets.
390        let mut stylesheet_remove_set = HashSet::with_capacity(adopted_stylesheets.len());
391
392        // Remove the old stylesheets from the StyleSet. This workflow is limited by utilities
393        // Stylo StyleSet given to us.
394        // TODO(stevennovaryo): we could optimize this by maintaining the longest common prefix
395        //                      but we should consider the implementation of ObservableArray as well.
396        for sheet_to_remove in adopted_stylesheets.iter() {
397            // Check for duplicates, only proceed with the removal if the stylesheet is not removed yet.
398            if stylesheet_remove_set.insert(sheet_to_remove) {
399                owner.remove_stylesheet(
400                    StylesheetSource::Constructed(sheet_to_remove.clone()),
401                    &sheet_to_remove.style_stylesheet(),
402                );
403                sheet_to_remove.remove_adopter(owner);
404            }
405        }
406
407        // The set to check for the duplicates when adding a new stylesheet.
408        let mut stylesheet_add_set = HashSet::with_capacity(incoming_stylesheets.len());
409
410        // Readd all stylesheet to the StyleSet. This workflow is limited by the utilities
411        // Stylo StyleSet given to us.
412        for sheet in incoming_stylesheets.iter() {
413            // Check for duplicates.
414            if !stylesheet_add_set.insert(sheet) {
415                // The idea is that this case is rare, so we pay the price of removing the
416                // old sheet from the styles and append it later rather than the other way
417                // around.
418                owner.remove_stylesheet(
419                    StylesheetSource::Constructed(sheet.clone()),
420                    &sheet.style_stylesheet(),
421                );
422            } else {
423                sheet.add_adopter(owner.clone());
424            }
425
426            owner.append_constructed_stylesheet(sheet);
427        }
428
429        *adopted_stylesheets = incoming_stylesheets.to_vec();
430
431        Ok(())
432    }
433
434    /// Set adoptedStylesheet given a js value by converting and passing the converted
435    /// values to the inner [DocumentOrShadowRoot::set_adopted_stylesheet].
436    pub(crate) fn set_adopted_stylesheet_from_jsval(
437        context: JSContext,
438        adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
439        incoming_value: HandleValue,
440        owner: &StyleSheetListOwner,
441    ) -> ErrorResult {
442        let maybe_stylesheets =
443            Vec::<DomRoot<CSSStyleSheet>>::safe_from_jsval(context, incoming_value, ());
444
445        match maybe_stylesheets {
446            Ok(ConversionResult::Success(stylesheets)) => {
447                rooted_vec!(let stylesheets <- stylesheets.to_owned().iter().map(|s| s.as_traced()));
448
449                DocumentOrShadowRoot::set_adopted_stylesheet(
450                    adopted_stylesheets,
451                    &stylesheets,
452                    owner,
453                )
454            },
455            Ok(ConversionResult::Failure(msg)) => Err(Error::Type(msg.to_string())),
456            Err(_) => Err(Error::Type(
457                "The provided value is not a sequence of 'CSSStylesheet'.".to_owned(),
458            )),
459        }
460    }
461}