Skip to main content

script/dom/css/
cssstyledeclaration.rs

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