Skip to main content

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