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::{CanGc, JSContext};
15use servo_arc::Arc;
16use servo_config::pref;
17use style::media_queries::MediaList;
18use style::shared_lock::{SharedRwLock as StyleSharedRwLock, SharedRwLockReadGuard};
19use style::stylesheets::scope_rule::ImplicitScopeRoot;
20use style::stylesheets::{Stylesheet, StylesheetContents};
21use stylo_atoms::Atom;
22use webrender_api::units::LayoutPoint;
23
24use super::bindings::trace::HashMapTracedValues;
25use crate::dom::bindings::cell::DomRefCell;
26use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
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::css::stylesheetlist::StyleSheetListOwner;
34use crate::dom::element::Element;
35use crate::dom::html::htmlelement::HTMLElement;
36use crate::dom::node::{self, Node, VecPreOrderInsertionHelper};
37use crate::dom::shadowroot::ShadowRoot;
38use crate::dom::types::{CSSStyleSheet, EventTarget};
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 = "Stylo"]
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 ::style::stylesheets::StylesheetInDocument for ServoStylesheetInDocument {
104    fn enabled(&self) -> bool {
105        self.sheet.enabled()
106    }
107
108    fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
109        self.sheet.media(guard)
110    }
111
112    fn contents<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a StylesheetContents {
113        self.sheet.contents(guard)
114    }
115
116    fn implicit_scope_root(&self) -> Option<ImplicitScopeRoot> {
117        None
118    }
119}
120
121// https://w3c.github.io/webcomponents/spec/shadow/#extensions-to-the-documentorshadowroot-mixin
122#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
123#[derive(Clone, JSTraceable, MallocSizeOf)]
124pub(crate) struct DocumentOrShadowRoot {
125    window: Dom<Window>,
126}
127
128impl DocumentOrShadowRoot {
129    pub(crate) fn new(window: &Window) -> Self {
130        Self {
131            window: Dom::from_ref(window),
132        }
133    }
134
135    #[expect(unsafe_code)]
136    // https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
137    pub(crate) fn element_from_point(
138        &self,
139        x: Finite<f64>,
140        y: Finite<f64>,
141        document_element: Option<DomRoot<Element>>,
142        has_browsing_context: bool,
143    ) -> Option<DomRoot<Element>> {
144        let x = *x as f32;
145        let y = *y as f32;
146        let viewport = self.window.viewport_details().size;
147
148        if !has_browsing_context {
149            return None;
150        }
151
152        if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
153            return None;
154        }
155
156        let results = self
157            .window
158            .elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::empty());
159        let Some(result) = results.first() else {
160            return document_element;
161        };
162
163        // SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
164        // layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
165        let address = UntrustedNodeAddress(result.node.0 as *const c_void);
166        let node = unsafe { node::from_untrusted_node_address(address) };
167        DomRoot::downcast::<Element>(node.clone()).or_else(|| {
168            let parent_node = node.GetParentNode()?;
169            if let Some(shadow_root) = parent_node.downcast::<ShadowRoot>() {
170                Some(shadow_root.Host())
171            } else {
172                node.GetParentElement()
173            }
174        })
175    }
176
177    #[expect(unsafe_code)]
178    // https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
179    pub(crate) fn elements_from_point(
180        &self,
181        x: Finite<f64>,
182        y: Finite<f64>,
183        document_element: Option<DomRoot<Element>>,
184        has_browsing_context: bool,
185    ) -> Vec<DomRoot<Element>> {
186        let x = *x as f32;
187        let y = *y as f32;
188        let viewport = self.window.viewport_details().size;
189
190        if !has_browsing_context {
191            return vec![];
192        }
193
194        // Step 2
195        if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
196            return vec![];
197        }
198
199        // Step 1 and Step 3
200        let nodes = self
201            .window
202            .elements_from_point_query(LayoutPoint::new(x, y), ElementsFromPointFlags::FindAll);
203        let mut elements: Vec<DomRoot<Element>> = nodes
204            .iter()
205            .flat_map(|result| {
206                // SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
207                // layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
208                let address = UntrustedNodeAddress(result.node.0 as *const c_void);
209                let node = unsafe { node::from_untrusted_node_address(address) };
210                DomRoot::downcast::<Element>(node)
211            })
212            .collect();
213
214        // Step 4
215        if let Some(root_element) = document_element {
216            if elements.last() != Some(&root_element) {
217                elements.push(root_element);
218            }
219        }
220
221        // Step 5
222        elements
223    }
224
225    // https://html.spec.whatwg.org/multipage/#dom-document-activeelement
226    pub(crate) fn get_active_element(
227        &self,
228        focused_element: Option<DomRoot<Element>>,
229        body: Option<DomRoot<HTMLElement>>,
230        document_element: Option<DomRoot<Element>>,
231    ) -> Option<DomRoot<Element>> {
232        // TODO: Step 2.
233
234        match focused_element {
235            Some(element) => Some(element), // Step 3. and 4.
236            None => match body {
237                // Step 5.
238                Some(body) => Some(DomRoot::upcast(body)),
239                None => document_element,
240            },
241        }
242    }
243
244    /// Remove a stylesheet owned by `owner` from the list of document sheets.
245    #[cfg_attr(crown, expect(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
246    pub(crate) fn remove_stylesheet(
247        owner: StylesheetSource,
248        s: &Arc<Stylesheet>,
249        mut stylesheets: StylesheetSetRef<ServoStylesheetInDocument>,
250    ) {
251        let guard = s.shared_lock.read();
252
253        // FIXME(emilio): Would be nice to remove the clone, etc.
254        stylesheets.remove_stylesheet(
255            None,
256            ServoStylesheetInDocument {
257                sheet: s.clone(),
258                owner,
259            },
260            &guard,
261        );
262    }
263
264    /// Add a stylesheet owned by `owner` to the list of document sheets, in the
265    /// correct tree position.
266    #[cfg_attr(crown, expect(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
267    pub(crate) fn add_stylesheet(
268        owner: StylesheetSource,
269        mut stylesheets: StylesheetSetRef<ServoStylesheetInDocument>,
270        sheet: Arc<Stylesheet>,
271        insertion_point: Option<ServoStylesheetInDocument>,
272        style_shared_lock: &StyleSharedRwLock,
273    ) {
274        debug_assert!(owner.is_a_valid_owner(), "Wat");
275
276        if owner.is_constructed() && !pref!(dom_adoptedstylesheet_enabled) {
277            return;
278        }
279
280        let sheet = ServoStylesheetInDocument { sheet, owner };
281
282        let guard = style_shared_lock.read();
283
284        match insertion_point {
285            Some(ip) => {
286                stylesheets.insert_stylesheet_before(None, sheet, ip, &guard);
287            },
288            None => {
289                stylesheets.append_stylesheet(None, sheet, &guard);
290            },
291        }
292    }
293
294    /// Remove any existing association between the provided id/name and any elements in this document.
295    pub(crate) fn unregister_named_element(
296        &self,
297        id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
298        to_unregister: &Element,
299        id: &Atom,
300    ) {
301        debug!(
302            "Removing named element {:p}: {:p} id={}",
303            self, to_unregister, id
304        );
305        let mut id_map = id_map.borrow_mut();
306        let is_empty = match id_map.get_mut(id) {
307            None => false,
308            Some(elements) => {
309                let position = elements
310                    .iter()
311                    .position(|element| &**element == to_unregister)
312                    .expect("This element should be in registered.");
313                elements.remove(position);
314                elements.is_empty()
315            },
316        };
317        if is_empty {
318            id_map.remove(id);
319        }
320    }
321
322    /// Associate an element present in this document with the provided id/name.
323    pub(crate) fn register_named_element(
324        &self,
325        id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
326        element: &Element,
327        id: &Atom,
328        root: DomRoot<Node>,
329    ) {
330        debug!("Adding named element {:p}: {:p} id={}", self, element, id);
331        assert!(
332            element.upcast::<Node>().is_in_a_document_tree() ||
333                element.upcast::<Node>().is_in_a_shadow_tree()
334        );
335        assert!(!id.is_empty());
336        let mut id_map = id_map.borrow_mut();
337        let elements = id_map.entry(id.clone()).or_default();
338        elements.insert_pre_order(element, &root);
339    }
340
341    /// Inner part of adopted stylesheet. We are setting it by, assuming it is a FrozenArray
342    /// instead of an ObservableArray. Thus, it would have a completely different workflow
343    /// compared to the spec. The workflow here is actually following Gecko's implementation
344    /// of AdoptedStylesheet before the implementation of ObservableArray.
345    ///
346    /// The main purpose from this function is to set the `&mut adopted_stylesheet` to match
347    /// `incoming_stylesheet` and update the corresponding Styleset in a Document or a ShadowRoot.
348    /// In case of duplicates, the setter will respect the last duplicates.
349    ///
350    /// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets>
351    // TODO: Handle duplicated adoptedstylesheet correctly, Stylo is preventing duplicates inside a
352    //       Stylesheet Set. But this is not ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1978755
353    fn set_adopted_stylesheet(
354        adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
355        incoming_stylesheets: &[Dom<CSSStyleSheet>],
356        owner: &StyleSheetListOwner,
357    ) -> ErrorResult {
358        if !pref!(dom_adoptedstylesheet_enabled) {
359            return Ok(());
360        }
361
362        let owner_doc = match owner {
363            StyleSheetListOwner::Document(doc) => doc,
364            StyleSheetListOwner::ShadowRoot(root) => root.owner_doc(),
365        };
366
367        for sheet in incoming_stylesheets.iter() {
368            // > If value’s constructed flag is not set, or its constructor document is not equal
369            // > to this DocumentOrShadowRoot’s node document, throw a "NotAllowedError" DOMException.
370            if !sheet.constructor_document_matches(owner_doc) {
371                return Err(Error::NotAllowed(None));
372            }
373        }
374
375        // The set to check for the duplicates when removing the old stylesheets.
376        let mut stylesheet_remove_set = HashSet::with_capacity(adopted_stylesheets.len());
377
378        // Remove the old stylesheets from the StyleSet. This workflow is limited by utilities
379        // Stylo StyleSet given to us.
380        // TODO(stevennovaryo): we could optimize this by maintaining the longest common prefix
381        //                      but we should consider the implementation of ObservableArray as well.
382        for sheet_to_remove in adopted_stylesheets.iter() {
383            // Check for duplicates, only proceed with the removal if the stylesheet is not removed yet.
384            if stylesheet_remove_set.insert(sheet_to_remove) {
385                owner.remove_stylesheet(
386                    StylesheetSource::Constructed(sheet_to_remove.clone()),
387                    &sheet_to_remove.style_stylesheet(),
388                );
389                sheet_to_remove.remove_adopter(owner);
390            }
391        }
392
393        // The set to check for the duplicates when adding a new stylesheet.
394        let mut stylesheet_add_set = HashSet::with_capacity(incoming_stylesheets.len());
395
396        // Readd all stylesheet to the StyleSet. This workflow is limited by the utilities
397        // Stylo StyleSet given to us.
398        for sheet in incoming_stylesheets.iter() {
399            // Check for duplicates.
400            if !stylesheet_add_set.insert(sheet) {
401                // The idea is that this case is rare, so we pay the price of removing the
402                // old sheet from the styles and append it later rather than the other way
403                // around.
404                owner.remove_stylesheet(
405                    StylesheetSource::Constructed(sheet.clone()),
406                    &sheet.style_stylesheet(),
407                );
408            } else {
409                sheet.add_adopter(owner.clone());
410            }
411
412            owner.append_constructed_stylesheet(sheet);
413        }
414
415        *adopted_stylesheets = incoming_stylesheets.to_vec();
416
417        Ok(())
418    }
419
420    /// Set adoptedStylesheet given a js value by converting and passing the converted
421    /// values to the inner [DocumentOrShadowRoot::set_adopted_stylesheet].
422    pub(crate) fn set_adopted_stylesheet_from_jsval(
423        context: JSContext,
424        adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
425        incoming_value: HandleValue,
426        owner: &StyleSheetListOwner,
427        can_gc: CanGc,
428    ) -> ErrorResult {
429        let maybe_stylesheets =
430            Vec::<DomRoot<CSSStyleSheet>>::safe_from_jsval(context, incoming_value, (), can_gc);
431
432        match maybe_stylesheets {
433            Ok(ConversionResult::Success(stylesheets)) => {
434                rooted_vec!(let stylesheets <- stylesheets.to_owned().iter().map(|s| s.as_traced()));
435
436                DocumentOrShadowRoot::set_adopted_stylesheet(
437                    adopted_stylesheets,
438                    &stylesheets,
439                    owner,
440                )
441            },
442            Ok(ConversionResult::Failure(msg)) => Err(Error::Type(msg.into_owned())),
443            Err(_) => Err(Error::Type(
444                c"The provided value is not a sequence of 'CSSStylesheet'.".to_owned(),
445            )),
446        }
447    }
448
449    /// <https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement>
450    pub(crate) fn get_fullscreen_element(
451        node: &Node,
452        fullscreen_element: Option<DomRoot<Element>>,
453    ) -> Option<DomRoot<Element>> {
454        // Step 1. If this is a shadow root and its host is not connected, then return null.
455        if let Some(shadow_root) = node.downcast::<ShadowRoot>() {
456            if !shadow_root.Host().is_connected() {
457                return None;
458            }
459        }
460
461        // Step 2. Let candidate be the result of retargeting fullscreen element against this.
462        let retargeted = fullscreen_element?
463            .upcast::<EventTarget>()
464            .retarget(node.upcast());
465        // It's safe to unwrap downcasting to `Element` because `retarget` either returns `fullscreen_element` or a host of `fullscreen_element` and hosts are always elements.
466        let candidate = DomRoot::downcast::<Element>(retargeted).unwrap();
467
468        // Step 3. If candidate and this are in the same tree, then return candidate.
469        if *candidate
470            .upcast::<Node>()
471            .GetRootNode(&GetRootNodeOptions::empty()) ==
472            *node
473        {
474            return Some(candidate);
475        }
476
477        // Step 4. Return null.
478        None
479    }
480}