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