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