Skip to main content

script/dom/css/
cssstyledeclaration.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::RefCell;
6use std::cmp::Ordering;
7use std::sync::LazyLock;
8
9use dom_struct::dom_struct;
10use html5ever::local_name;
11use js::context::JSContext;
12use script_bindings::reflector::{Reflector, reflect_dom_object};
13use servo_arc::Arc;
14use servo_url::ServoUrl;
15use style::attr::AttrValue;
16use style::properties::{
17    Importance, LonghandId, PropertyDeclarationBlock, PropertyId, ShorthandId,
18    SourcePropertyDeclaration, parse_one_declaration_into, parse_style_attribute,
19};
20use style::selector_parser::PseudoElement;
21use style::shared_lock::Locked;
22use style::stylesheets::{CssRuleType, Origin, StylesheetInDocument, UrlExtraData};
23use style_traits::ParsingMode;
24
25use super::cssrule::CSSRule;
26use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
27use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
28use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
29use crate::dom::bindings::inheritance::Castable;
30use crate::dom::bindings::reflector::DomGlobal;
31use crate::dom::bindings::root::{Dom, DomRoot};
32use crate::dom::bindings::str::DOMString;
33use crate::dom::element::Element;
34use crate::dom::node::{Node, NodeTraits};
35use crate::dom::types::CSSFontFaceDescriptors;
36use crate::dom::window::Window;
37
38// http://dev.w3.org/csswg/cssom/#the-cssstyledeclaration-interface
39#[dom_struct]
40pub(crate) struct CSSStyleDeclaration {
41    reflector_: Reflector,
42    owner: CSSStyleOwner,
43    readonly: bool,
44    #[no_trace]
45    pseudo: Option<PseudoElement>,
46}
47
48#[derive(JSTraceable, MallocSizeOf)]
49#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
50pub(crate) enum CSSStyleOwner {
51    /// Used when calling `getComputedStyle()` with an invalid pseudo-element selector.
52    /// See <https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle>
53    Null,
54    Element(Dom<Element>),
55    CSSRule(
56        Dom<CSSRule>,
57        #[ignore_malloc_size_of = "Stylo"]
58        #[no_trace]
59        RefCell<Arc<Locked<PropertyDeclarationBlock>>>,
60    ),
61}
62
63impl CSSStyleOwner {
64    // Mutate the declaration block associated to this style owner, and
65    // optionally indicate if it has changed (assumed to be true).
66    fn mutate_associated_block<F, R>(&self, cx: &mut JSContext, f: F) -> R
67    where
68        F: FnOnce(&mut PropertyDeclarationBlock, &mut bool) -> R,
69    {
70        // TODO(emilio): This has some duplication just to avoid dummy clones.
71        //
72        // This is somewhat complex but the complexity is encapsulated.
73        let mut changed = true;
74        match *self {
75            CSSStyleOwner::Null => unreachable!(
76                "CSSStyleDeclaration should always be read-only when CSSStyleOwner is Null"
77            ),
78            CSSStyleOwner::Element(ref element) => {
79                let document = element.owner_document();
80                let shared_lock = document.style_shared_author_lock();
81                let mut attribute_pdb =
82                    element.style_attribute().safe_borrow_mut(cx.no_gc()).take();
83
84                // When there are mutation observers, the old PDB and the new PDB need to
85                // co-exist so that both attribute values can be serialized and sent to
86                // the observer. In that case, first clone the block and mutate that instead
87                // of mutating the existing one.
88                if element.needs_preserved_style_attribute_after_change() {
89                    attribute_pdb = attribute_pdb.map(|attribute_pdb| {
90                        let new_pdb = attribute_pdb.read_with(&shared_lock.read()).clone();
91                        Arc::new(shared_lock.wrap(new_pdb))
92                    });
93                }
94
95                let (attribute_pdb, result) = if let Some(attribute_pdb) = attribute_pdb {
96                    let mut guard = shared_lock.write();
97                    let writable_pdb = attribute_pdb.write_with(&mut guard);
98                    let result = f(writable_pdb, &mut changed);
99                    (attribute_pdb, result)
100                } else {
101                    let mut new_pdb = PropertyDeclarationBlock::new();
102                    let result = f(&mut new_pdb, &mut changed);
103
104                    changed = !new_pdb.declarations().is_empty();
105                    if !changed {
106                        return result;
107                    }
108
109                    (Arc::new(shared_lock.wrap(new_pdb)), result)
110                };
111
112                // If nothing changed, replace the unchanged PDB in the attribute and just
113                // return. Note that there's no need to remove the attribute here if the
114                // declaration block is empty (see
115                // https://github.com/whatwg/html/issues/2306).
116                if !changed {
117                    *element.style_attribute().safe_borrow_mut(cx.no_gc()) = Some(attribute_pdb);
118                    return result;
119                }
120
121                element.set_attribute(
122                    cx,
123                    &local_name!("style"),
124                    AttrValue::from_declaration(attribute_pdb, shared_lock.clone()),
125                );
126                result
127            },
128            CSSStyleOwner::CSSRule(ref rule, ref pdb) => {
129                rule.parent_stylesheet().will_modify();
130                let result = {
131                    let mut guard = rule.shared_lock().write();
132                    f(&mut *pdb.borrow().write_with(&mut guard), &mut changed)
133                };
134                if changed {
135                    rule.parent_stylesheet().notify_invalidations(cx.no_gc());
136                }
137                result
138            },
139        }
140    }
141
142    fn with_block<F, R>(&self, f: F) -> R
143    where
144        F: FnOnce(&PropertyDeclarationBlock) -> R,
145    {
146        match *self {
147            CSSStyleOwner::Null => {
148                unreachable!("Should never call with_block for CSStyleOwner::Null")
149            },
150            CSSStyleOwner::Element(ref el) => match *el.style_attribute().borrow() {
151                Some(ref pdb) => {
152                    let document = el.owner_document();
153                    let guard = document.style_shared_author_lock().read();
154                    f(pdb.read_with(&guard))
155                },
156                None => {
157                    let pdb = PropertyDeclarationBlock::new();
158                    f(&pdb)
159                },
160            },
161            CSSStyleOwner::CSSRule(ref rule, ref pdb) => {
162                let guard = rule.shared_lock().read();
163                f(pdb.borrow().read_with(&guard))
164            },
165        }
166    }
167
168    fn window(&self) -> DomRoot<Window> {
169        match *self {
170            CSSStyleOwner::Null => {
171                unreachable!("Should never try to access window of CSStyleOwner::Null")
172            },
173            CSSStyleOwner::Element(ref el) => el.owner_window(),
174            CSSStyleOwner::CSSRule(ref rule, _) => DomRoot::from_ref(rule.global().as_window()),
175        }
176    }
177
178    fn base_url(&self) -> ServoUrl {
179        match *self {
180            CSSStyleOwner::Null => {
181                unreachable!("Should never try to access base URL of CSStyleOwner::Null")
182            },
183            CSSStyleOwner::Element(ref el) => el.owner_document().base_url(),
184            CSSStyleOwner::CSSRule(ref rule, _) => ServoUrl::from({
185                let guard = rule.shared_lock().read();
186                rule.parent_stylesheet()
187                    .style_stylesheet()
188                    .contents(&guard)
189                    .url_data
190                    .0
191                    .clone()
192            }),
193        }
194    }
195}
196
197#[derive(MallocSizeOf, PartialEq)]
198pub(crate) enum CSSModificationAccess {
199    ReadWrite,
200    Readonly,
201}
202
203macro_rules! css_properties(
204    ( $([$getter:ident, $setter:ident, $id:expr],)* ) => (
205        $(
206            fn $getter(&self) -> DOMString {
207                debug_assert!(
208                    $id.enabled_for_all_content(),
209                    "Someone forgot a #[Pref] annotation"
210                );
211                self.get_property_value($id)
212            }
213            fn $setter(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
214                debug_assert!(
215                    $id.enabled_for_all_content(),
216                    "Someone forgot a #[Pref] annotation"
217                );
218                self.set_property(cx, $id, value, DOMString::new())
219            }
220        )*
221    );
222);
223
224fn remove_property(decls: &mut PropertyDeclarationBlock, id: &PropertyId) -> bool {
225    let first_declaration = decls.first_declaration_to_remove(id);
226    let first_declaration = match first_declaration {
227        Some(i) => i,
228        None => return false,
229    };
230    decls.remove_property(id, first_declaration);
231    true
232}
233
234impl CSSStyleDeclaration {
235    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
236    pub(crate) fn new_inherited(
237        owner: CSSStyleOwner,
238        pseudo: Option<PseudoElement>,
239        modification_access: CSSModificationAccess,
240    ) -> CSSStyleDeclaration {
241        // If creating a CSSStyleDeclaration with CSSSStyleOwner::Null, this should always
242        // be in read-only mode.
243        assert!(
244            !matches!(owner, CSSStyleOwner::Null) ||
245                modification_access == CSSModificationAccess::Readonly
246        );
247
248        CSSStyleDeclaration {
249            reflector_: Reflector::new(),
250            owner,
251            readonly: modification_access == CSSModificationAccess::Readonly,
252            pseudo,
253        }
254    }
255
256    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
257    pub(crate) fn new(
258        cx: &mut JSContext,
259        global: &Window,
260        owner: CSSStyleOwner,
261        pseudo: Option<PseudoElement>,
262        modification_access: CSSModificationAccess,
263    ) -> DomRoot<CSSStyleDeclaration> {
264        reflect_dom_object(
265            cx,
266            Box::new(CSSStyleDeclaration::new_inherited(
267                owner,
268                pseudo,
269                modification_access,
270            )),
271            global,
272        )
273    }
274
275    pub(crate) fn update_property_declaration_block(
276        &self,
277        pdb: &Arc<Locked<PropertyDeclarationBlock>>,
278    ) {
279        if let CSSStyleOwner::CSSRule(_, pdb_cell) = &self.owner {
280            *pdb_cell.borrow_mut() = pdb.clone();
281        } else {
282            panic!("update_rule called on CSSStyleDeclaration with a Element owner");
283        }
284    }
285
286    fn get_computed_style(&self, property: PropertyId) -> DOMString {
287        match self.owner {
288            CSSStyleOwner::CSSRule(..) => {
289                panic!("get_computed_style called on CSSStyleDeclaration with a CSSRule owner")
290            },
291            CSSStyleOwner::Element(ref el) => {
292                let node = el.upcast::<Node>();
293                if !node.is_connected() {
294                    return DOMString::new();
295                }
296                let addr = node.to_trusted_node_address();
297                node.owner_window()
298                    .resolved_style_query(addr, self.pseudo, property)
299            },
300            CSSStyleOwner::Null => DOMString::new(),
301        }
302    }
303
304    fn get_property_value(&self, id: PropertyId) -> DOMString {
305        if matches!(self.owner, CSSStyleOwner::Null) {
306            return DOMString::new();
307        }
308
309        if self.readonly {
310            // Readonly style declarations are used for getComputedStyle.
311            return self.get_computed_style(id);
312        }
313
314        let mut string = String::new();
315
316        self.owner.with_block(|pdb| {
317            pdb.property_value_to_css(&id, &mut string).unwrap();
318        });
319
320        DOMString::from(string)
321    }
322
323    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty>
324    fn set_property(
325        &self,
326        cx: &mut JSContext,
327        id: PropertyId,
328        value: DOMString,
329        priority: DOMString,
330    ) -> ErrorResult {
331        self.set_property_inner(cx, PotentiallyParsedPropertyId::Parsed(id), value, priority)
332    }
333
334    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty>
335    ///
336    /// This function receives a `PotentiallyParsedPropertyId` instead of a `DOMString` in case
337    /// the caller already has a parsed property ID.
338    fn set_property_inner(
339        &self,
340        cx: &mut JSContext,
341        id: PotentiallyParsedPropertyId,
342        value: DOMString,
343        priority: DOMString,
344    ) -> ErrorResult {
345        // Step 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
346        if self.readonly {
347            return Err(Error::NoModificationAllowed(Some(
348                "This CSS style declaration is read-only".into(),
349            )));
350        }
351
352        let id = match id {
353            PotentiallyParsedPropertyId::Parsed(id) => {
354                if !id.enabled_for_all_content() {
355                    return Ok(());
356                }
357
358                id
359            },
360            PotentiallyParsedPropertyId::NotParsed(unparsed) => {
361                match PropertyId::parse_enabled_for_all_content(&unparsed.str()) {
362                    Ok(id) => id,
363                    Err(..) => return Ok(()),
364                }
365            },
366        };
367        let base_url = UrlExtraData(self.owner.base_url().get_arc());
368        self.owner.mutate_associated_block(cx, |pdb, changed| {
369            // Step 3. If value is the empty string, invoke removeProperty()
370            // with property as argument and return.
371            if value.is_empty() {
372                *changed = remove_property(pdb, &id);
373                return Ok(());
374            }
375
376            // Step 4. If priority is not the empty string and is not an ASCII case-insensitive
377            // match for the string "important", then return.
378            let importance = match &*priority.str() {
379                "" => Importance::Normal,
380                p if p.eq_ignore_ascii_case("important") => Importance::Important,
381                _ => {
382                    *changed = false;
383                    return Ok(());
384                },
385            };
386
387            // Step 5
388            let window = self.owner.window();
389            let quirks_mode = window.Document().quirks_mode();
390            let mut declarations = SourcePropertyDeclaration::default();
391            let result = parse_one_declaration_into(
392                &mut declarations,
393                id,
394                &value.str(),
395                Origin::Author,
396                &base_url,
397                Some(window.css_error_reporter()),
398                ParsingMode::DEFAULT,
399                quirks_mode,
400                CssRuleType::Style,
401            );
402
403            // Step 6
404            match result {
405                Ok(()) => {},
406                Err(_) => {
407                    *changed = false;
408                    return Ok(());
409                },
410            }
411
412            let mut updates = Default::default();
413            *changed = pdb.prepare_for_update(&declarations, importance, &mut updates);
414
415            if !*changed {
416                return Ok(());
417            }
418
419            // Step 7
420            // Step 8
421            pdb.update(declarations.drain(), importance, &mut updates);
422
423            Ok(())
424        })
425    }
426}
427
428pub(crate) static ENABLED_LONGHAND_PROPERTIES: LazyLock<Vec<LonghandId>> = LazyLock::new(|| {
429    // The 'all' shorthand contains all the enabled longhands with 2 exceptions:
430    // 'direction' and 'unicode-bidi', so these must be added afterward.
431    let mut enabled_longhands: Vec<LonghandId> = ShorthandId::All.longhands().collect();
432    if PropertyId::NonCustom(LonghandId::Direction.into()).enabled_for_all_content() {
433        enabled_longhands.push(LonghandId::Direction);
434    }
435    if PropertyId::NonCustom(LonghandId::UnicodeBidi.into()).enabled_for_all_content() {
436        enabled_longhands.push(LonghandId::UnicodeBidi);
437    }
438
439    // Sort lexicographically, but with vendor-prefixed properties after standard ones.
440    enabled_longhands.sort_unstable_by(|a, b| {
441        let a = a.name();
442        let b = b.name();
443        let is_a_vendor_prefixed = a.starts_with('-');
444        let is_b_vendor_prefixed = b.starts_with('-');
445        if is_a_vendor_prefixed == is_b_vendor_prefixed {
446            a.partial_cmp(b).unwrap()
447        } else if is_b_vendor_prefixed {
448            Ordering::Less
449        } else {
450            Ordering::Greater
451        }
452    });
453    enabled_longhands
454});
455
456enum PotentiallyParsedPropertyId {
457    Parsed(PropertyId),
458    NotParsed(DOMString),
459}
460
461impl CSSStyleDeclarationMethods<crate::DomTypeHolder> for CSSStyleDeclaration {
462    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-length>
463    fn Length(&self) -> u32 {
464        if matches!(self.owner, CSSStyleOwner::Null) {
465            return 0;
466        }
467
468        if self.readonly {
469            // Readonly style declarations are used for getComputedStyle.
470            // TODO: include custom properties whose computed value is not the guaranteed-invalid value.
471            return ENABLED_LONGHAND_PROPERTIES.len() as u32;
472        }
473        self.owner.with_block(|pdb| pdb.declarations().len() as u32)
474    }
475
476    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-item>
477    fn Item(&self, index: u32) -> DOMString {
478        self.IndexedGetter(index).unwrap_or_default()
479    }
480
481    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-getpropertyvalue>
482    fn GetPropertyValue(&self, property: DOMString) -> DOMString {
483        if let Some(css_font_face_descriptors) = self.downcast::<CSSFontFaceDescriptors>() {
484            css_font_face_descriptors.get_property_value(&property.str())
485        } else {
486            let Ok(id) = PropertyId::parse_enabled_for_all_content(&property.str()) else {
487                return DOMString::new();
488            };
489            self.get_property_value(id)
490        }
491    }
492
493    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-getpropertypriority>
494    fn GetPropertyPriority(&self, property: DOMString) -> DOMString {
495        if self.is::<CSSFontFaceDescriptors>() {
496            // Font face descriptors do not have priorities.
497            // https://searchfox.org/firefox-main/rev/91c8ca3faa6ccbb72d65d89401fd31fd3313afc4/layout/style/CSSFontFaceRule.cpp#84-89
498            return DOMString::new();
499        }
500
501        if self.readonly {
502            // Readonly style declarations are used for getComputedStyle.
503            return DOMString::new();
504        }
505        let id = match PropertyId::parse_enabled_for_all_content(&property.str()) {
506            Ok(id) => id,
507            Err(..) => return DOMString::new(),
508        };
509
510        self.owner.with_block(|pdb| {
511            if pdb.property_priority(&id).important() {
512                DOMString::from_static("important")
513            } else {
514                // Step 4
515                DOMString::new()
516            }
517        })
518    }
519
520    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-setproperty>
521    fn SetProperty(
522        &self,
523        cx: &mut JSContext,
524        property: DOMString,
525        value: DOMString,
526        priority: DOMString,
527    ) -> ErrorResult {
528        self.set_property_inner(
529            cx,
530            PotentiallyParsedPropertyId::NotParsed(property),
531            value,
532            priority,
533        )
534    }
535
536    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-removeproperty>
537    fn RemoveProperty(&self, cx: &mut JSContext, property: DOMString) -> Fallible<DOMString> {
538        // Step 1
539        if self.readonly {
540            return Err(Error::NoModificationAllowed(Some(
541                "This CSS style declaration is read-only".into(),
542            )));
543        }
544
545        let id = match PropertyId::parse_enabled_for_all_content(&property.str()) {
546            Ok(id) => id,
547            Err(..) => return Ok(DOMString::new()),
548        };
549
550        let mut string = String::new();
551        self.owner.mutate_associated_block(cx, |pdb, changed| {
552            pdb.property_value_to_css(&id, &mut string).unwrap();
553            *changed = remove_property(pdb, &id);
554        });
555
556        // Step 6
557        Ok(DOMString::from(string))
558    }
559
560    /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat>
561    fn CssFloat(&self) -> DOMString {
562        self.get_property_value(PropertyId::NonCustom(LonghandId::Float.into()))
563    }
564
565    /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat>
566    fn SetCssFloat(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
567        self.set_property(
568            cx,
569            PropertyId::NonCustom(LonghandId::Float.into()),
570            value,
571            DOMString::new(),
572        )
573    }
574
575    /// <https://dev.w3.org/csswg/cssom/#the-cssstyledeclaration-interface>
576    fn IndexedGetter(&self, index: u32) -> Option<DOMString> {
577        if matches!(self.owner, CSSStyleOwner::Null) {
578            return None;
579        }
580        if self.readonly {
581            // Readonly style declarations are used for getComputedStyle.
582            // TODO: include custom properties whose computed value is not the guaranteed-invalid value.
583            let longhand = ENABLED_LONGHAND_PROPERTIES.get(index as usize)?;
584            return Some(DOMString::from(longhand.name()));
585        }
586        self.owner.with_block(|pdb| {
587            let declaration = pdb.declarations().get(index as usize)?;
588            Some(DOMString::from(declaration.id().name()))
589        })
590    }
591
592    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext>
593    fn CssText(&self) -> DOMString {
594        if self.readonly {
595            // Readonly style declarations are used for getComputedStyle.
596            return DOMString::new();
597        }
598        self.owner.with_block(|pdb| {
599            let mut serialization = String::new();
600            pdb.to_css(&mut serialization).unwrap();
601            DOMString::from(serialization)
602        })
603    }
604
605    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext>
606    fn SetCssText(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
607        // Step 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
608        if self.readonly {
609            return Err(Error::NoModificationAllowed(Some(
610                "This CSS style declaration is read-only".into(),
611            )));
612        }
613
614        let window = self.owner.window();
615        let quirks_mode = window.Document().quirks_mode();
616        let base_url = UrlExtraData(self.owner.base_url().get_arc());
617        self.owner.mutate_associated_block(cx, |pdb, _changed| {
618            // Step 3
619            *pdb = parse_style_attribute(
620                &value.str(),
621                &base_url,
622                Some(window.css_error_reporter()),
623                quirks_mode,
624                CssRuleType::Style,
625            );
626        });
627
628        Ok(())
629    }
630
631    // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-_camel_cased_attribute
632    style::css_properties_accessors!(css_properties);
633}