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