script/dom/
cssstylesheet.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::cell::{Cell, Ref};
6use std::rc::Rc;
7
8use dom_struct::dom_struct;
9use js::rust::HandleObject;
10use script_bindings::inheritance::Castable;
11use script_bindings::realms::InRealm;
12use script_bindings::root::Dom;
13use servo_arc::Arc;
14use style::media_queries::MediaList as StyleMediaList;
15use style::shared_lock::{SharedRwLock, SharedRwLockReadGuard};
16use style::stylesheets::{
17    AllowImportRules, CssRuleTypes, Origin, Stylesheet as StyleStyleSheet, UrlExtraData,
18};
19
20use crate::dom::bindings::cell::DomRefCell;
21use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::{
22    CSSStyleSheetInit, CSSStyleSheetMethods,
23};
24use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
25use crate::dom::bindings::codegen::GenericBindings::CSSRuleListBinding::CSSRuleList_Binding::CSSRuleListMethods;
26use crate::dom::bindings::codegen::UnionTypes::MediaListOrString;
27use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::{
30    DomGlobal, reflect_dom_object, reflect_dom_object_with_proto,
31};
32use crate::dom::bindings::root::{DomRoot, MutNullableDom};
33use crate::dom::bindings::str::{DOMString, USVString};
34use crate::dom::cssrulelist::{CSSRuleList, RulesSource};
35use crate::dom::document::Document;
36use crate::dom::element::Element;
37use crate::dom::html::htmlstyleelement::HTMLStyleElement;
38use crate::dom::medialist::MediaList;
39use crate::dom::node::NodeTraits;
40use crate::dom::stylesheet::StyleSheet;
41use crate::dom::stylesheetlist::StyleSheetListOwner;
42use crate::dom::types::Promise;
43use crate::dom::window::Window;
44use crate::script_runtime::CanGc;
45use crate::test::TrustedPromise;
46
47#[dom_struct]
48pub(crate) struct CSSStyleSheet {
49    stylesheet: StyleSheet,
50
51    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-owner-node>
52    owner_node: MutNullableDom<Element>,
53
54    /// <https://drafts.csswg.org/cssom/#ref-for-concept-css-style-sheet-css-rules>
55    rulelist: MutNullableDom<CSSRuleList>,
56
57    /// The inner Stylo's [Stylesheet].
58    #[ignore_malloc_size_of = "Arc"]
59    #[no_trace]
60    style_stylesheet: DomRefCell<Arc<StyleStyleSheet>>,
61
62    /// The inner Stylo's [SharedRwLock], stored at here to avoid referencing
63    /// temporary variables.
64    #[no_trace]
65    style_shared_lock: SharedRwLock,
66
67    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-origin-clean-flag>
68    origin_clean: Cell<bool>,
69
70    /// In which [Document] that this stylesheet was constructed.
71    ///
72    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-constructor-document>
73    constructor_document: Option<Dom<Document>>,
74
75    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-disallow-modification-flag>
76    disallow_modification: Cell<bool>,
77
78    /// Documents or shadow DOMs thats adopt this stylesheet, they will be notified whenever
79    /// the stylesheet is modified.
80    adopters: DomRefCell<Vec<StyleSheetListOwner>>,
81}
82
83impl CSSStyleSheet {
84    fn new_inherited(
85        owner: Option<&Element>,
86        type_: DOMString,
87        href: Option<DOMString>,
88        title: Option<DOMString>,
89        stylesheet: Arc<StyleStyleSheet>,
90        constructor_document: Option<&Document>,
91    ) -> CSSStyleSheet {
92        CSSStyleSheet {
93            stylesheet: StyleSheet::new_inherited(type_, href, title),
94            owner_node: MutNullableDom::new(owner),
95            rulelist: MutNullableDom::new(None),
96            style_shared_lock: stylesheet.shared_lock.clone(),
97            style_stylesheet: DomRefCell::new(stylesheet),
98            origin_clean: Cell::new(true),
99            constructor_document: constructor_document.map(Dom::from_ref),
100            adopters: Default::default(),
101            disallow_modification: Cell::new(false),
102        }
103    }
104
105    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
106    #[allow(clippy::too_many_arguments)]
107    pub(crate) fn new(
108        window: &Window,
109        owner: Option<&Element>,
110        type_: DOMString,
111        href: Option<DOMString>,
112        title: Option<DOMString>,
113        stylesheet: Arc<StyleStyleSheet>,
114        constructor_document: Option<&Document>,
115        can_gc: CanGc,
116    ) -> DomRoot<CSSStyleSheet> {
117        reflect_dom_object(
118            Box::new(CSSStyleSheet::new_inherited(
119                owner,
120                type_,
121                href,
122                title,
123                stylesheet,
124                constructor_document,
125            )),
126            window,
127            can_gc,
128        )
129    }
130
131    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
132    #[allow(clippy::too_many_arguments)]
133    fn new_with_proto(
134        window: &Window,
135        proto: Option<HandleObject>,
136        owner: Option<&Element>,
137        type_: DOMString,
138        href: Option<DOMString>,
139        title: Option<DOMString>,
140        stylesheet: Arc<StyleStyleSheet>,
141        constructor_document: Option<&Document>,
142        can_gc: CanGc,
143    ) -> DomRoot<CSSStyleSheet> {
144        reflect_dom_object_with_proto(
145            Box::new(CSSStyleSheet::new_inherited(
146                owner,
147                type_,
148                href,
149                title,
150                stylesheet,
151                constructor_document,
152            )),
153            window,
154            proto,
155            can_gc,
156        )
157    }
158
159    fn rulelist(&self, can_gc: CanGc) -> DomRoot<CSSRuleList> {
160        self.rulelist.or_init(|| {
161            let rules = self.style_stylesheet.borrow().contents.rules.clone();
162            CSSRuleList::new(
163                self.global().as_window(),
164                self,
165                RulesSource::Rules(rules),
166                can_gc,
167            )
168        })
169    }
170
171    pub(crate) fn disabled(&self) -> bool {
172        self.style_stylesheet.borrow().disabled()
173    }
174
175    pub(crate) fn owner_node(&self) -> Option<DomRoot<Element>> {
176        self.owner_node.get()
177    }
178
179    pub(crate) fn set_disabled(&self, disabled: bool) {
180        if self.style_stylesheet.borrow().set_disabled(disabled) {
181            self.notify_invalidations();
182        }
183    }
184
185    pub(crate) fn set_owner_node(&self, value: Option<&Element>) {
186        self.owner_node.set(value);
187    }
188
189    pub(crate) fn shared_lock(&self) -> &SharedRwLock {
190        &self.style_shared_lock
191    }
192
193    pub(crate) fn style_stylesheet(&self) -> Ref<'_, Arc<StyleStyleSheet>> {
194        self.style_stylesheet.borrow()
195    }
196
197    pub(crate) fn set_origin_clean(&self, origin_clean: bool) {
198        self.origin_clean.set(origin_clean);
199    }
200
201    pub(crate) fn medialist(&self, can_gc: CanGc) -> DomRoot<MediaList> {
202        MediaList::new(
203            self.global().as_window(),
204            self,
205            self.style_stylesheet().media.clone(),
206            can_gc,
207        )
208    }
209
210    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-constructed-flag>
211    #[inline]
212    pub(crate) fn is_constructed(&self) -> bool {
213        self.constructor_document.is_some()
214    }
215
216    pub(crate) fn constructor_document_matches(&self, other_doc: &Document) -> bool {
217        match &self.constructor_document {
218            Some(doc) => *doc == other_doc,
219            None => false,
220        }
221    }
222
223    /// Add a [StyleSheetListOwner] as an adopter to be notified whenever this stylesheet is
224    /// modified.
225    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
226    pub(crate) fn add_adopter(&self, owner: StyleSheetListOwner) {
227        debug_assert!(self.is_constructed());
228        self.adopters.borrow_mut().push(owner);
229    }
230
231    pub(crate) fn remove_adopter(&self, owner: &StyleSheetListOwner) {
232        let adopters = &mut *self.adopters.borrow_mut();
233        if let Some(index) = adopters.iter().position(|o| o == owner) {
234            adopters.swap_remove(index);
235        }
236    }
237
238    pub(crate) fn will_modify(&self) {
239        let Some(node) = self.owner_node.get() else {
240            return;
241        };
242
243        let Some(node) = node.downcast::<HTMLStyleElement>() else {
244            return;
245        };
246
247        node.will_modify_stylesheet();
248    }
249
250    pub(crate) fn update_style_stylesheet(
251        &self,
252        style_stylesheet: &Arc<StyleStyleSheet>,
253        guard: &SharedRwLockReadGuard,
254    ) {
255        // When the shared `StylesheetContents` is about to be modified,
256        // `CSSStyleSheet::owner_node` performs a copy-on-write to avoid
257        // affecting other sharers, see `CSSStyleSheet::will_modify`. And
258        // then updates the references to `CssRule` or `PropertyDeclarationBlock`
259        // stored in the CSSOMs to ensure that modifications are made only
260        // on the new copy.
261        *self.style_stylesheet.borrow_mut() = style_stylesheet.clone();
262        if let Some(rulelist) = self.rulelist.get() {
263            let rules = style_stylesheet.contents.rules.clone();
264            rulelist.update_rules(RulesSource::Rules(rules), guard);
265        }
266    }
267
268    /// Invalidate all stylesheet set this stylesheet is a part on.
269    pub(crate) fn notify_invalidations(&self) {
270        if let Some(owner) = self.owner_node() {
271            owner.stylesheet_list_owner().invalidate_stylesheets();
272        }
273        for adopter in self.adopters.borrow().iter() {
274            adopter.invalidate_stylesheets();
275        }
276    }
277
278    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-disallow-modification-flag>
279    pub(crate) fn disallow_modification(&self) -> bool {
280        self.disallow_modification.get()
281    }
282}
283
284impl CSSStyleSheetMethods<crate::DomTypeHolder> for CSSStyleSheet {
285    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-cssstylesheet>
286    fn Constructor(
287        window: &Window,
288        proto: Option<HandleObject>,
289        can_gc: CanGc,
290        options: &CSSStyleSheetInit,
291    ) -> DomRoot<Self> {
292        let doc = window.Document();
293        let shared_lock = doc.style_shared_lock().clone();
294        let media = Arc::new(shared_lock.wrap(match &options.media {
295            Some(media) => match media {
296                MediaListOrString::MediaList(media_list) => media_list.clone_media_list(),
297                MediaListOrString::String(str) => MediaList::parse_media_list(str, window),
298            },
299            None => StyleMediaList::empty(),
300        }));
301        let stylesheet = Arc::new(StyleStyleSheet::from_str(
302            "",
303            UrlExtraData(window.get_url().get_arc()),
304            Origin::Author,
305            media,
306            shared_lock,
307            None,
308            window.css_error_reporter(),
309            doc.quirks_mode(),
310            AllowImportRules::No,
311        ));
312        if options.disabled {
313            stylesheet.set_disabled(true);
314        }
315        Self::new_with_proto(
316            window,
317            proto,
318            None, // owner
319            "text/css".into(),
320            None, // href
321            None, // title
322            stylesheet,
323            Some(&window.Document()), // constructor_document
324            can_gc,
325        )
326    }
327
328    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-cssrules>
329    fn GetCssRules(&self, can_gc: CanGc) -> Fallible<DomRoot<CSSRuleList>> {
330        if !self.origin_clean.get() {
331            return Err(Error::Security);
332        }
333        Ok(self.rulelist(can_gc))
334    }
335
336    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule>
337    fn InsertRule(&self, rule: DOMString, index: u32, can_gc: CanGc) -> Fallible<u32> {
338        // Step 1. If the origin-clean flag is unset, throw a SecurityError exception.
339        if !self.origin_clean.get() {
340            return Err(Error::Security);
341        }
342
343        // Step 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
344        if self.disallow_modification() {
345            return Err(Error::NotAllowed);
346        }
347
348        self.rulelist(can_gc)
349            .insert_rule(&rule, index, CssRuleTypes::default(), None, can_gc)
350    }
351
352    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-deleterule>
353    fn DeleteRule(&self, index: u32, can_gc: CanGc) -> ErrorResult {
354        // Step 1. If the origin-clean flag is unset, throw a SecurityError exception.
355        if !self.origin_clean.get() {
356            return Err(Error::Security);
357        }
358
359        // Step 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
360        if self.disallow_modification() {
361            return Err(Error::NotAllowed);
362        }
363        self.rulelist(can_gc).remove_rule(index)
364    }
365
366    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-rules>
367    fn GetRules(&self, can_gc: CanGc) -> Fallible<DomRoot<CSSRuleList>> {
368        self.GetCssRules(can_gc)
369    }
370
371    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-removerule>
372    fn RemoveRule(&self, index: u32, can_gc: CanGc) -> ErrorResult {
373        self.DeleteRule(index, can_gc)
374    }
375
376    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-addrule>
377    fn AddRule(
378        &self,
379        selector: DOMString,
380        block: DOMString,
381        optional_index: Option<u32>,
382        can_gc: CanGc,
383    ) -> Fallible<i32> {
384        // > 1. Let *rule* be an empty string.
385        // > 2. Append *selector* to *rule*.
386        let mut rule = selector;
387
388        // > 3. Append " { " to *rule*.
389        // > 4. If *block* is not empty, append *block*, followed by a space, to *rule*.
390        // > 5. Append "}" to *rule*.
391        if block.is_empty() {
392            rule.push_str(" { }");
393        } else {
394            rule.push_str(" { ");
395            rule.push_str(block.str());
396            rule.push_str(" }");
397        };
398
399        // > 6. Let *index* be *optionalIndex* if provided, or the number of CSS rules in the stylesheet otherwise.
400        let index = optional_index.unwrap_or_else(|| self.rulelist(can_gc).Length());
401
402        // > 7. Call `insertRule()`, with *rule* and *index* as arguments.
403        self.InsertRule(rule, index, can_gc)?;
404
405        // > 8. Return -1.
406        Ok(-1)
407    }
408
409    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-replace>
410    fn Replace(&self, text: USVString, comp: InRealm, can_gc: CanGc) -> Fallible<Rc<Promise>> {
411        // Step 1. Let promise be a promise.
412        let promise = Promise::new_in_current_realm(comp, can_gc);
413
414        // Step 2. If the constructed flag is not set, or the disallow modification flag is set,
415        // reject promise with a NotAllowedError DOMException and return promise.
416        if !self.is_constructed() || self.disallow_modification() {
417            return Err(Error::NotAllowed);
418        }
419
420        // Step 3. Set the disallow modification flag.
421        self.disallow_modification.set(true);
422
423        // Step 4. In parallel, do these steps:
424        let trusted_sheet = Trusted::new(self);
425        let trusted_promise = TrustedPromise::new(promise.clone());
426
427        self.global()
428            .task_manager()
429            .dom_manipulation_task_source()
430            .queue(task!(cssstylesheet_replace: move || {
431                let sheet = trusted_sheet.root();
432
433                // Step 4.1. Let rules be the result of running parse a stylesheet’s contents from text.
434                // Step 4.2. If rules contains one or more @import rules, remove those rules from rules.
435                // We are disallowing import rules in parsing
436                let global = sheet.global();
437                let window = global.as_window();
438
439                sheet.will_modify();
440
441                #[cfg(feature = "tracing")]
442                let _span = tracing::trace_span!("ParseStylesheet", servo_profiling = true).entered();
443                StyleStyleSheet::update_from_str(
444                    &sheet.style_stylesheet(),
445                    &text,
446                    UrlExtraData(window.get_url().get_arc()),
447                    None,
448                    window.css_error_reporter(),
449                    AllowImportRules::No,
450                );
451
452                // Step 4.3. Set sheet’s CSS rules to rules.
453                // We reset our rule list, which will be initialized properly
454                // at the next getter access.
455                sheet.rulelist.set(None);
456
457                // Notify invalidation to update the styles immediately.
458                sheet.notify_invalidations();
459
460                // Step 4.4. Unset sheet’s disallow modification flag.
461                sheet.disallow_modification.set(false);
462
463                // Step 4.5. Resolve promise with sheet.
464                trusted_promise.root().resolve_native(&sheet, CanGc::note());
465            }));
466
467        Ok(promise)
468    }
469
470    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-replacesync>
471    fn ReplaceSync(&self, text: USVString) -> Result<(), Error> {
472        // Step 1. If the constructed flag is not set, or the disallow modification flag is set,
473        // throw a NotAllowedError DOMException.
474        if !self.is_constructed() || self.disallow_modification() {
475            return Err(Error::NotAllowed);
476        }
477
478        // Step 2. Let rules be the result of running parse a stylesheet’s contents from text.
479        let global = self.global();
480        let window = global.as_window();
481
482        self.will_modify();
483
484        #[cfg(feature = "tracing")]
485        let _span = tracing::trace_span!("ParseStylesheet", servo_profiling = true).entered();
486        StyleStyleSheet::update_from_str(
487            &self.style_stylesheet(),
488            &text,
489            UrlExtraData(window.get_url().get_arc()),
490            None,
491            window.css_error_reporter(),
492            AllowImportRules::No, // Step 3.If rules contains one or more @import rules, remove those rules from rules.
493        );
494
495        // Step 4. Set sheet’s CSS rules to rules.
496        // We reset our rule list, which will be initialized properly
497        // at the next getter access.
498        self.rulelist.set(None);
499
500        // Notify invalidation to update the styles immediately.
501        self.notify_invalidations();
502
503        Ok(())
504    }
505}