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 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, StylesheetInDocument, UrlExtraData};
21use style_traits::ParsingMode;
22
23use super::cssrule::CSSRule;
24use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
25use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
26use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
29use crate::dom::bindings::root::{Dom, DomRoot};
30use crate::dom::bindings::str::DOMString;
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 = "Stylo"]
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                let guard = rule.shared_lock().read();
180                rule.parent_stylesheet()
181                    .style_stylesheet()
182                    .contents(&guard)
183                    .url_data
184                    .0
185                    .clone()
186            }),
187        }
188    }
189}
190
191#[derive(MallocSizeOf, PartialEq)]
192pub(crate) enum CSSModificationAccess {
193    ReadWrite,
194    Readonly,
195}
196
197macro_rules! css_properties(
198    ( $([$getter:ident, $setter:ident, $id:expr],)* ) => (
199        $(
200            fn $getter(&self) -> DOMString {
201                debug_assert!(
202                    $id.enabled_for_all_content(),
203                    "Someone forgot a #[Pref] annotation"
204                );
205                self.get_property_value($id)
206            }
207            fn $setter(&self, value: DOMString) -> ErrorResult {
208                debug_assert!(
209                    $id.enabled_for_all_content(),
210                    "Someone forgot a #[Pref] annotation"
211                );
212                self.set_property($id, value, DOMString::new(), CanGc::note())
213            }
214        )*
215    );
216);
217
218fn remove_property(decls: &mut PropertyDeclarationBlock, id: &PropertyId) -> bool {
219    let first_declaration = decls.first_declaration_to_remove(id);
220    let first_declaration = match first_declaration {
221        Some(i) => i,
222        None => return false,
223    };
224    decls.remove_property(id, first_declaration);
225    true
226}
227
228impl CSSStyleDeclaration {
229    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
230    pub(crate) fn new_inherited(
231        owner: CSSStyleOwner,
232        pseudo: Option<PseudoElement>,
233        modification_access: CSSModificationAccess,
234    ) -> CSSStyleDeclaration {
235        // If creating a CSSStyleDeclaration with CSSSStyleOwner::Null, this should always
236        // be in read-only mode.
237        assert!(
238            !matches!(owner, CSSStyleOwner::Null) ||
239                modification_access == CSSModificationAccess::Readonly
240        );
241
242        CSSStyleDeclaration {
243            reflector_: Reflector::new(),
244            owner,
245            readonly: modification_access == CSSModificationAccess::Readonly,
246            pseudo,
247        }
248    }
249
250    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
251    pub(crate) fn new(
252        global: &Window,
253        owner: CSSStyleOwner,
254        pseudo: Option<PseudoElement>,
255        modification_access: CSSModificationAccess,
256        can_gc: CanGc,
257    ) -> DomRoot<CSSStyleDeclaration> {
258        reflect_dom_object(
259            Box::new(CSSStyleDeclaration::new_inherited(
260                owner,
261                pseudo,
262                modification_access,
263            )),
264            global,
265            can_gc,
266        )
267    }
268
269    pub(crate) fn update_property_declaration_block(
270        &self,
271        pdb: &Arc<Locked<PropertyDeclarationBlock>>,
272    ) {
273        if let CSSStyleOwner::CSSRule(_, pdb_cell) = &self.owner {
274            *pdb_cell.borrow_mut() = pdb.clone();
275        } else {
276            panic!("update_rule called on CSSStyleDeclaration with a Element owner");
277        }
278    }
279
280    fn get_computed_style(&self, property: PropertyId) -> DOMString {
281        match self.owner {
282            CSSStyleOwner::CSSRule(..) => {
283                panic!("get_computed_style called on CSSStyleDeclaration with a CSSRule owner")
284            },
285            CSSStyleOwner::Element(ref el) => {
286                let node = el.upcast::<Node>();
287                if !node.is_connected() {
288                    return DOMString::new();
289                }
290                let addr = node.to_trusted_node_address();
291                node.owner_window()
292                    .resolved_style_query(addr, self.pseudo, property)
293            },
294            CSSStyleOwner::Null => DOMString::new(),
295        }
296    }
297
298    fn get_property_value(&self, id: PropertyId) -> DOMString {
299        if matches!(self.owner, CSSStyleOwner::Null) {
300            return DOMString::new();
301        }
302
303        if self.readonly {
304            // Readonly style declarations are used for getComputedStyle.
305            return self.get_computed_style(id);
306        }
307
308        let mut string = String::new();
309
310        self.owner.with_block(|pdb| {
311            pdb.property_value_to_css(&id, &mut string).unwrap();
312        });
313
314        DOMString::from(string)
315    }
316
317    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty>
318    fn set_property(
319        &self,
320        id: PropertyId,
321        value: DOMString,
322        priority: DOMString,
323        can_gc: CanGc,
324    ) -> ErrorResult {
325        self.set_property_inner(
326            PotentiallyParsedPropertyId::Parsed(id),
327            value,
328            priority,
329            can_gc,
330        )
331    }
332
333    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty>
334    ///
335    /// This function receives a `PotentiallyParsedPropertyId` instead of a `DOMString` in case
336    /// the caller already has a parsed property ID.
337    fn set_property_inner(
338        &self,
339        id: PotentiallyParsedPropertyId,
340        value: DOMString,
341        priority: DOMString,
342        can_gc: CanGc,
343    ) -> ErrorResult {
344        // Step 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
345        if self.readonly {
346            return Err(Error::NoModificationAllowed(None));
347        }
348
349        let id = match id {
350            PotentiallyParsedPropertyId::Parsed(id) => {
351                if !id.enabled_for_all_content() {
352                    return Ok(());
353                }
354
355                id
356            },
357            PotentiallyParsedPropertyId::NotParsed(unparsed) => {
358                match PropertyId::parse_enabled_for_all_content(&unparsed.str()) {
359                    Ok(id) => id,
360                    Err(..) => return Ok(()),
361                }
362            },
363        };
364        let base_url = UrlExtraData(self.owner.base_url().get_arc());
365        self.owner.mutate_associated_block(
366            |pdb, changed| {
367                // Step 3. If value is the empty string, invoke removeProperty()
368                // with property as argument and return.
369                if value.is_empty() {
370                    *changed = remove_property(pdb, &id);
371                    return Ok(());
372                }
373
374                // Step 4. If priority is not the empty string and is not an ASCII case-insensitive
375                // match for the string "important", then return.
376                let importance = match &*priority.str() {
377                    "" => Importance::Normal,
378                    p if p.eq_ignore_ascii_case("important") => Importance::Important,
379                    _ => {
380                        *changed = false;
381                        return Ok(());
382                    },
383                };
384
385                // Step 5
386                let window = self.owner.window();
387                let quirks_mode = window.Document().quirks_mode();
388                let mut declarations = SourcePropertyDeclaration::default();
389                let result = parse_one_declaration_into(
390                    &mut declarations,
391                    id,
392                    &value.str(),
393                    Origin::Author,
394                    &base_url,
395                    Some(window.css_error_reporter()),
396                    ParsingMode::DEFAULT,
397                    quirks_mode,
398                    CssRuleType::Style,
399                );
400
401                // Step 6
402                match result {
403                    Ok(()) => {},
404                    Err(_) => {
405                        *changed = false;
406                        return Ok(());
407                    },
408                }
409
410                let mut updates = Default::default();
411                *changed = pdb.prepare_for_update(&declarations, importance, &mut updates);
412
413                if !*changed {
414                    return Ok(());
415                }
416
417                // Step 7
418                // Step 8
419                pdb.update(declarations.drain(), importance, &mut updates);
420
421                Ok(())
422            },
423            can_gc,
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        let id = match PropertyId::parse_enabled_for_all_content(&property.str()) {
484            Ok(id) => id,
485            Err(..) => return DOMString::new(),
486        };
487        self.get_property_value(id)
488    }
489
490    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-getpropertypriority>
491    fn GetPropertyPriority(&self, property: DOMString) -> DOMString {
492        if self.readonly {
493            // Readonly style declarations are used for getComputedStyle.
494            return DOMString::new();
495        }
496        let id = match PropertyId::parse_enabled_for_all_content(&property.str()) {
497            Ok(id) => id,
498            Err(..) => return DOMString::new(),
499        };
500
501        self.owner.with_block(|pdb| {
502            if pdb.property_priority(&id).important() {
503                DOMString::from("important")
504            } else {
505                // Step 4
506                DOMString::new()
507            }
508        })
509    }
510
511    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-setproperty>
512    fn SetProperty(
513        &self,
514        property: DOMString,
515        value: DOMString,
516        priority: DOMString,
517        can_gc: CanGc,
518    ) -> ErrorResult {
519        self.set_property_inner(
520            PotentiallyParsedPropertyId::NotParsed(property),
521            value,
522            priority,
523            can_gc,
524        )
525    }
526
527    /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-removeproperty>
528    fn RemoveProperty(&self, property: DOMString, can_gc: CanGc) -> Fallible<DOMString> {
529        // Step 1
530        if self.readonly {
531            return Err(Error::NoModificationAllowed(None));
532        }
533
534        let id = match PropertyId::parse_enabled_for_all_content(&property.str()) {
535            Ok(id) => id,
536            Err(..) => return Ok(DOMString::new()),
537        };
538
539        let mut string = String::new();
540        self.owner.mutate_associated_block(
541            |pdb, changed| {
542                pdb.property_value_to_css(&id, &mut string).unwrap();
543                *changed = remove_property(pdb, &id);
544            },
545            can_gc,
546        );
547
548        // Step 6
549        Ok(DOMString::from(string))
550    }
551
552    /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat>
553    fn CssFloat(&self) -> DOMString {
554        self.get_property_value(PropertyId::NonCustom(LonghandId::Float.into()))
555    }
556
557    /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat>
558    fn SetCssFloat(&self, value: DOMString, can_gc: CanGc) -> ErrorResult {
559        self.set_property(
560            PropertyId::NonCustom(LonghandId::Float.into()),
561            value,
562            DOMString::new(),
563            can_gc,
564        )
565    }
566
567    /// <https://dev.w3.org/csswg/cssom/#the-cssstyledeclaration-interface>
568    fn IndexedGetter(&self, index: u32) -> Option<DOMString> {
569        if matches!(self.owner, CSSStyleOwner::Null) {
570            return None;
571        }
572        if self.readonly {
573            // Readonly style declarations are used for getComputedStyle.
574            // TODO: include custom properties whose computed value is not the guaranteed-invalid value.
575            let longhand = ENABLED_LONGHAND_PROPERTIES.get(index as usize)?;
576            return Some(DOMString::from(longhand.name()));
577        }
578        self.owner.with_block(|pdb| {
579            let declaration = pdb.declarations().get(index as usize)?;
580            Some(DOMString::from(declaration.id().name()))
581        })
582    }
583
584    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext>
585    fn CssText(&self) -> DOMString {
586        if self.readonly {
587            // Readonly style declarations are used for getComputedStyle.
588            return DOMString::new();
589        }
590        self.owner.with_block(|pdb| {
591            let mut serialization = String::new();
592            pdb.to_css(&mut serialization).unwrap();
593            DOMString::from(serialization)
594        })
595    }
596
597    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext>
598    fn SetCssText(&self, value: DOMString, can_gc: CanGc) -> ErrorResult {
599        let window = self.owner.window();
600
601        // Step 1
602        if self.readonly {
603            return Err(Error::NoModificationAllowed(None));
604        }
605
606        let quirks_mode = window.Document().quirks_mode();
607        let base_url = UrlExtraData(self.owner.base_url().get_arc());
608        self.owner.mutate_associated_block(
609            |pdb, _changed| {
610                // Step 3
611                *pdb = parse_style_attribute(
612                    &value.str(),
613                    &base_url,
614                    Some(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}