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