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