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, cx: &mut JSContext) -> Option<DomRoot<CSSStyleSheet>> {
51        match self {
52            StylesheetSource::Element(el) => el.upcast::<Node>().get_cssom_stylesheet(cx),
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: &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        adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
376        incoming_stylesheets: &[Dom<CSSStyleSheet>],
377        owner: &StyleSheetListOwner,
378    ) -> ErrorResult {
379        if !pref!(dom_adoptedstylesheet_enabled) {
380            return Ok(());
381        }
382
383        let owner_doc = match owner {
384            StyleSheetListOwner::Document(doc) => doc,
385            StyleSheetListOwner::ShadowRoot(root) => root.owner_doc(),
386        };
387
388        for sheet in incoming_stylesheets.iter() {
389            // > If value’s constructed flag is not set, or its constructor document is not equal
390            // > to this DocumentOrShadowRoot’s node document, throw a "NotAllowedError" DOMException.
391            if !sheet.constructor_document_matches(owner_doc) {
392                return Err(Error::NotAllowed(None));
393            }
394        }
395
396        // The set to check for the duplicates when removing the old stylesheets.
397        let mut stylesheet_remove_set = HashSet::with_capacity(adopted_stylesheets.len());
398
399        // Remove the old stylesheets from the StyleSet. This workflow is limited by utilities
400        // Stylo StyleSet given to us.
401        // TODO(stevennovaryo): we could optimize this by maintaining the longest common prefix
402        //                      but we should consider the implementation of ObservableArray as well.
403        for sheet_to_remove in adopted_stylesheets.iter() {
404            // Check for duplicates, only proceed with the removal if the stylesheet is not removed yet.
405            if stylesheet_remove_set.insert(sheet_to_remove) {
406                owner.remove_stylesheet(
407                    StylesheetSource::Constructed(sheet_to_remove.clone()),
408                    &sheet_to_remove.style_stylesheet(),
409                );
410                sheet_to_remove.remove_adopter(owner);
411            }
412        }
413
414        // The set to check for the duplicates when adding a new stylesheet.
415        let mut stylesheet_add_set = HashSet::with_capacity(incoming_stylesheets.len());
416
417        // Readd all stylesheet to the StyleSet. This workflow is limited by the utilities
418        // Stylo StyleSet given to us.
419        for sheet in incoming_stylesheets.iter() {
420            // Check for duplicates.
421            if !stylesheet_add_set.insert(sheet) {
422                // The idea is that this case is rare, so we pay the price of removing the
423                // old sheet from the styles and append it later rather than the other way
424                // around.
425                owner.remove_stylesheet(
426                    StylesheetSource::Constructed(sheet.clone()),
427                    &sheet.style_stylesheet(),
428                );
429            } else {
430                sheet.add_adopter(owner.clone());
431            }
432
433            owner.append_constructed_stylesheet(sheet);
434        }
435
436        *adopted_stylesheets = incoming_stylesheets.to_vec();
437
438        Ok(())
439    }
440
441    /// Set adoptedStylesheet given a js value by converting and passing the converted
442    /// values to the inner [DocumentOrShadowRoot::set_adopted_stylesheet].
443    pub(crate) fn set_adopted_stylesheet_from_jsval(
444        cx: &mut JSContext,
445        adopted_stylesheets: &mut Vec<Dom<CSSStyleSheet>>,
446        incoming_value: HandleValue,
447        owner: &StyleSheetListOwner,
448    ) -> ErrorResult {
449        let maybe_stylesheets =
450            Vec::<DomRoot<CSSStyleSheet>>::safe_from_jsval(cx, incoming_value, ())
451                .map_err(|_| Error::JSFailed)?;
452
453        match maybe_stylesheets {
454            ConversionResult::Success(stylesheets) => {
455                rooted_vec!(let stylesheets <- stylesheets.iter().map(|s| s.as_traced()));
456
457                DocumentOrShadowRoot::set_adopted_stylesheet(
458                    adopted_stylesheets,
459                    &stylesheets,
460                    owner,
461                )
462            },
463            ConversionResult::Failure(msg) => Err(Error::Type(msg.into_owned())),
464        }
465    }
466}