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