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