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