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