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_with_cx};
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::window::Window;
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, cx: &mut JSContext, f: F) -> 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 element) => {
78                let document = element.owner_document();
79                let shared_lock = document.style_shared_author_lock();
80                let mut attribute_pdb = element.style_attribute().borrow_mut().take();
81
82                // When there are mutation observers, the old PDB and the new PDB need to
83                // co-exist so that both attribute values can be serialized and sent to
84                // the observer. In that case, first clone the block and mutate that instead
85                // of mutating the existing one.
86                if element.needs_preserved_style_attribute_after_change() {
87                    attribute_pdb = attribute_pdb.map(|attribute_pdb| {
88                        let new_pdb = attribute_pdb.read_with(&shared_lock.read()).clone();
89                        Arc::new(shared_lock.wrap(new_pdb))
90                    });
91                }
92
93                let (attribute_pdb, result) = if let Some(attribute_pdb) = attribute_pdb {
94                    let mut guard = shared_lock.write();
95                    let writable_pdb = attribute_pdb.write_with(&mut guard);
96                    let result = f(writable_pdb, &mut changed);
97                    (attribute_pdb, result)
98                } else {
99                    let mut new_pdb = PropertyDeclarationBlock::new();
100                    let result = f(&mut new_pdb, &mut changed);
101
102                    changed = !new_pdb.declarations().is_empty();
103                    if !changed {
104                        return result;
105                    }
106
107                    (Arc::new(shared_lock.wrap(new_pdb)), result)
108                };
109
110                // If nothing changed, replace the unchanged PDB in the attribute and just
111                // return. Note that there's no need to remove the attribute here if the
112                // declaration block is empty (see
113                // https://github.com/whatwg/html/issues/2306).
114                if !changed {
115                    *element.style_attribute().borrow_mut() = Some(attribute_pdb);
116                    return result;
117                }
118
119                element.set_attribute(
120                    cx,
121                    &local_name!("style"),
122                    AttrValue::from_declaration(attribute_pdb, shared_lock.clone()),
123                );
124                result
125            },
126            CSSStyleOwner::CSSRule(ref rule, ref pdb) => {
127                rule.parent_stylesheet().will_modify(cx);
128                let result = {
129                    let mut guard = rule.shared_lock().write();
130                    f(&mut *pdb.borrow().write_with(&mut guard), &mut changed)
131                };
132                if changed {
133                    rule.parent_stylesheet().notify_invalidations();
134                }
135                result
136            },
137        }
138    }
139
140    fn with_block<F, R>(&self, f: F) -> R
141    where
142        F: FnOnce(&PropertyDeclarationBlock) -> R,
143    {
144        match *self {
145            CSSStyleOwner::Null => {
146                unreachable!("Should never call with_block for CSStyleOwner::Null")
147            },
148            CSSStyleOwner::Element(ref el) => match *el.style_attribute().borrow() {
149                Some(ref pdb) => {
150                    let document = el.owner_document();
151                    let guard = document.style_shared_author_lock().read();
152                    f(pdb.read_with(&guard))
153                },
154                None => {
155                    let pdb = PropertyDeclarationBlock::new();
156                    f(&pdb)
157                },
158            },
159            CSSStyleOwner::CSSRule(ref rule, ref pdb) => {
160                let guard = rule.shared_lock().read();
161                f(pdb.borrow().read_with(&guard))
162            },
163        }
164    }
165
166    fn window(&self) -> DomRoot<Window> {
167        match *self {
168            CSSStyleOwner::Null => {
169                unreachable!("Should never try to access window of CSStyleOwner::Null")
170            },
171            CSSStyleOwner::Element(ref el) => el.owner_window(),
172            CSSStyleOwner::CSSRule(ref rule, _) => DomRoot::from_ref(rule.global().as_window()),
173        }
174    }
175
176    fn base_url(&self) -> ServoUrl {
177        match *self {
178            CSSStyleOwner::Null => {
179                unreachable!("Should never try to access base URL of CSStyleOwner::Null")
180            },
181            CSSStyleOwner::Element(ref el) => el.owner_document().base_url(),
182            CSSStyleOwner::CSSRule(ref rule, _) => ServoUrl::from({
183                let guard = rule.shared_lock().read();
184                rule.parent_stylesheet()
185                    .style_stylesheet()
186                    .contents(&guard)
187                    .url_data
188                    .0
189                    .clone()
190            }),
191        }
192    }
193}
194
195#[derive(MallocSizeOf, PartialEq)]
196pub(crate) enum CSSModificationAccess {
197    ReadWrite,
198    Readonly,
199}
200
201macro_rules! css_properties(
202    ( $([$getter:ident, $setter:ident, $id:expr],)* ) => (
203        $(
204            fn $getter(&self) -> DOMString {
205                debug_assert!(
206                    $id.enabled_for_all_content(),
207                    "Someone forgot a #[Pref] annotation"
208                );
209                self.get_property_value($id)
210            }
211            fn $setter(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
212                debug_assert!(
213                    $id.enabled_for_all_content(),
214                    "Someone forgot a #[Pref] annotation"
215                );
216                self.set_property(cx, $id, value, DOMString::new())
217            }
218        )*
219    );
220);
221
222fn remove_property(decls: &mut PropertyDeclarationBlock, id: &PropertyId) -> bool {
223    let first_declaration = decls.first_declaration_to_remove(id);
224    let first_declaration = match first_declaration {
225        Some(i) => i,
226        None => return false,
227    };
228    decls.remove_property(id, first_declaration);
229    true
230}
231
232impl CSSStyleDeclaration {
233    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
234    pub(crate) fn new_inherited(
235        owner: CSSStyleOwner,
236        pseudo: Option<PseudoElement>,
237        modification_access: CSSModificationAccess,
238    ) -> CSSStyleDeclaration {
239        // If creating a CSSStyleDeclaration with CSSSStyleOwner::Null, this should always
240        // be in read-only mode.
241        assert!(
242            !matches!(owner, CSSStyleOwner::Null) ||
243                modification_access == CSSModificationAccess::Readonly
244        );
245
246        CSSStyleDeclaration {
247            reflector_: Reflector::new(),
248            owner,
249            readonly: modification_access == CSSModificationAccess::Readonly,
250            pseudo,
251        }
252    }
253
254    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
255    pub(crate) fn new(
256        cx: &mut JSContext,
257        global: &Window,
258        owner: CSSStyleOwner,
259        pseudo: Option<PseudoElement>,
260        modification_access: CSSModificationAccess,
261    ) -> DomRoot<CSSStyleDeclaration> {
262        reflect_dom_object_with_cx(
263            Box::new(CSSStyleDeclaration::new_inherited(
264                owner,
265                pseudo,
266                modification_access,
267            )),
268            global,
269            cx,
270        )
271    }
272
273    pub(crate) fn update_property_declaration_block(
274        &self,
275        pdb: &Arc<Locked<PropertyDeclarationBlock>>,
276    ) {
277        if let CSSStyleOwner::CSSRule(_, pdb_cell) = &self.owner {
278            *pdb_cell.borrow_mut() = pdb.clone();
279        } else {
280            panic!("update_rule called on CSSStyleDeclaration with a Element owner");
281        }
282    }
283
284    fn get_computed_style(&self, property: PropertyId) -> DOMString {
285        match self.owner {
286            CSSStyleOwner::CSSRule(..) => {
287                panic!("get_computed_style called on CSSStyleDeclaration with a CSSRule owner")
288            },
289            CSSStyleOwner::Element(ref el) => {
290                let node = el.upcast::<Node>();
291                if !node.is_connected() {
292                    return DOMString::new();
293                }
294                let addr = node.to_trusted_node_address();
295                node.owner_window()
296                    .resolved_style_query(addr, self.pseudo, property)
297            },
298            CSSStyleOwner::Null => DOMString::new(),
299        }
300    }
301
302    fn get_property_value(&self, id: PropertyId) -> DOMString {
303        if matches!(self.owner, CSSStyleOwner::Null) {
304            return DOMString::new();
305        }
306
307        if self.readonly {
308            // Readonly style declarations are used for getComputedStyle.
309            return self.get_computed_style(id);
310        }
311
312        let mut string = String::new();
313
314        self.owner.with_block(|pdb| {
315            pdb.property_value_to_css(&id, &mut string).unwrap();
316        });
317
318        DOMString::from(string)
319    }
320
321    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty>
322    fn set_property(
323        &self,
324        cx: &mut JSContext,
325        id: PropertyId,
326        value: DOMString,
327        priority: DOMString,
328    ) -> ErrorResult {
329        self.set_property_inner(cx, PotentiallyParsedPropertyId::Parsed(id), value, priority)
330    }
331
332    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty>
333    ///
334    /// This function receives a `PotentiallyParsedPropertyId` instead of a `DOMString` in case
335    /// the caller already has a parsed property ID.
336    fn set_property_inner(
337        &self,
338        cx: &mut JSContext,
339        id: PotentiallyParsedPropertyId,
340        value: DOMString,
341        priority: DOMString,
342    ) -> ErrorResult {
343        // Step 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
344        if self.readonly {
345            return Err(Error::NoModificationAllowed(None));
346        }
347
348        let id = match id {
349            PotentiallyParsedPropertyId::Parsed(id) => {
350                if !id.enabled_for_all_content() {
351                    return Ok(());
352                }
353
354                id
355            },
356            PotentiallyParsedPropertyId::NotParsed(unparsed) => {
357                match PropertyId::parse_enabled_for_all_content(&unparsed.str()) {
358                    Ok(id) => id,
359                    Err(..) => return Ok(()),
360                }
361            },
362        };
363        let base_url = UrlExtraData(self.owner.base_url().get_arc());
364        self.owner.mutate_associated_block(cx, |pdb, changed| {
365            // Step 3. If value is the empty string, invoke removeProperty()
366            // with property as argument and return.
367            if value.is_empty() {
368                *changed = remove_property(pdb, &id);
369                return Ok(());
370            }
371
372            // Step 4. If priority is not the empty string and is not an ASCII case-insensitive
373            // match for the string "important", then return.
374            let importance = match &*priority.str() {
375                "" => Importance::Normal,
376                p if p.eq_ignore_ascii_case("important") => Importance::Important,
377                _ => {
378                    *changed = false;
379                    return Ok(());
380                },
381            };
382
383            // Step 5
384            let window = self.owner.window();
385            let quirks_mode = window.Document().quirks_mode();
386            let mut declarations = SourcePropertyDeclaration::default();
387            let result = parse_one_declaration_into(
388                &mut declarations,
389                id,
390                &value.str(),
391                Origin::Author,
392                &base_url,
393                Some(window.css_error_reporter()),
394                ParsingMode::DEFAULT,
395                quirks_mode,
396                CssRuleType::Style,
397            );
398
399            // Step 6
400            match result {
401                Ok(()) => {},
402                Err(_) => {
403                    *changed = false;
404                    return Ok(());
405                },
406            }
407
408            let mut updates = Default::default();
409            *changed = pdb.prepare_for_update(&declarations, importance, &mut updates);
410
411            if !*changed {
412                return Ok(());
413            }
414
415            // Step 7
416            // Step 8
417            pdb.update(declarations.drain(), importance, &mut updates);
418
419            Ok(())
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(cx, |pdb, changed| {
537            pdb.property_value_to_css(&id, &mut string).unwrap();
538            *changed = remove_property(pdb, &id);
539        });
540
541        // Step 6
542        Ok(DOMString::from(string))
543    }
544
545    /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat>
546    fn CssFloat(&self) -> DOMString {
547        self.get_property_value(PropertyId::NonCustom(LonghandId::Float.into()))
548    }
549
550    /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat>
551    fn SetCssFloat(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
552        self.set_property(
553            cx,
554            PropertyId::NonCustom(LonghandId::Float.into()),
555            value,
556            DOMString::new(),
557        )
558    }
559
560    /// <https://dev.w3.org/csswg/cssom/#the-cssstyledeclaration-interface>
561    fn IndexedGetter(&self, index: u32) -> Option<DOMString> {
562        if matches!(self.owner, CSSStyleOwner::Null) {
563            return None;
564        }
565        if self.readonly {
566            // Readonly style declarations are used for getComputedStyle.
567            // TODO: include custom properties whose computed value is not the guaranteed-invalid value.
568            let longhand = ENABLED_LONGHAND_PROPERTIES.get(index as usize)?;
569            return Some(DOMString::from(longhand.name()));
570        }
571        self.owner.with_block(|pdb| {
572            let declaration = pdb.declarations().get(index as usize)?;
573            Some(DOMString::from(declaration.id().name()))
574        })
575    }
576
577    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext>
578    fn CssText(&self) -> DOMString {
579        if self.readonly {
580            // Readonly style declarations are used for getComputedStyle.
581            return DOMString::new();
582        }
583        self.owner.with_block(|pdb| {
584            let mut serialization = String::new();
585            pdb.to_css(&mut serialization).unwrap();
586            DOMString::from(serialization)
587        })
588    }
589
590    /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext>
591    fn SetCssText(&self, cx: &mut JSContext, value: DOMString) -> ErrorResult {
592        let window = self.owner.window();
593
594        // Step 1
595        if self.readonly {
596            return Err(Error::NoModificationAllowed(None));
597        }
598
599        let quirks_mode = window.Document().quirks_mode();
600        let base_url = UrlExtraData(self.owner.base_url().get_arc());
601        self.owner.mutate_associated_block(cx, |pdb, _changed| {
602            // Step 3
603            *pdb = parse_style_attribute(
604                &value.str(),
605                &base_url,
606                Some(window.css_error_reporter()),
607                quirks_mode,
608                CssRuleType::Style,
609            );
610        });
611
612        Ok(())
613    }
614
615    // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-_camel_cased_attribute
616    style::css_properties_accessors!(css_properties);
617}