| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| use std::cell::RefCell; |
| use std::cmp::Ordering; |
| use std::sync::LazyLock; |
| |
| use dom_struct::dom_struct; |
| use html5ever::local_name; |
| use servo_arc::Arc; |
| use servo_url::ServoUrl; |
| use style::attr::AttrValue; |
| use style::properties::{ |
| Importance, LonghandId, PropertyDeclarationBlock, PropertyId, ShorthandId, |
| SourcePropertyDeclaration, parse_one_declaration_into, parse_style_attribute, |
| }; |
| use style::selector_parser::PseudoElement; |
| use style::shared_lock::Locked; |
| use style::stylesheets::{CssRuleType, Origin, UrlExtraData}; |
| use style_traits::ParsingMode; |
| |
| use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods; |
| use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; |
| use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; |
| use crate::dom::bindings::inheritance::Castable; |
| use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; |
| use crate::dom::bindings::root::{Dom, DomRoot}; |
| use crate::dom::bindings::str::DOMString; |
| use crate::dom::cssrule::CSSRule; |
| use crate::dom::element::Element; |
| use crate::dom::node::{Node, NodeTraits}; |
| use crate::dom::window::Window; |
| use crate::script_runtime::CanGc; |
| |
| // http://dev.w3.org/csswg/cssom/#the-cssstyledeclaration-interface |
| #[dom_struct] |
| pub(crate) struct CSSStyleDeclaration { |
| reflector_: Reflector, |
| owner: CSSStyleOwner, |
| readonly: bool, |
| #[no_trace] |
| pseudo: Option<PseudoElement>, |
| } |
| |
| #[derive(JSTraceable, MallocSizeOf)] |
| #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] |
| pub(crate) enum CSSStyleOwner { |
| /// Used when calling `getComputedStyle()` with an invalid pseudo-element selector. |
| /// See <https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle> |
| Null, |
| Element(Dom<Element>), |
| CSSRule( |
| Dom<CSSRule>, |
| #[ignore_malloc_size_of = "Arc"] |
| #[no_trace] |
| RefCell<Arc<Locked<PropertyDeclarationBlock>>>, |
| ), |
| } |
| |
| impl CSSStyleOwner { |
| // Mutate the declaration block associated to this style owner, and |
| // optionally indicate if it has changed (assumed to be true). |
| fn mutate_associated_block<F, R>(&self, f: F, can_gc: CanGc) -> R |
| where |
| F: FnOnce(&mut PropertyDeclarationBlock, &mut bool) -> R, |
| { |
| // TODO(emilio): This has some duplication just to avoid dummy clones. |
| // |
| // This is somewhat complex but the complexity is encapsulated. |
| let mut changed = true; |
| match *self { |
| CSSStyleOwner::Null => unreachable!( |
| "CSSStyleDeclaration should always be read-only when CSSStyleOwner is Null" |
| ), |
| CSSStyleOwner::Element(ref el) => { |
| let document = el.owner_document(); |
| let shared_lock = document.style_shared_lock(); |
| let mut attr = el.style_attribute().borrow_mut().take(); |
| let result = if let Some(lock) = attr.as_ref() { |
| let mut guard = shared_lock.write(); |
| let pdb = lock.write_with(&mut guard); |
| f(pdb, &mut changed) |
| } else { |
| let mut pdb = PropertyDeclarationBlock::new(); |
| let result = f(&mut pdb, &mut changed); |
| |
| // Here `changed` is somewhat silly, because we know the |
| // exact conditions under it changes. |
| changed = !pdb.declarations().is_empty(); |
| if changed { |
| attr = Some(Arc::new(shared_lock.wrap(pdb))); |
| } |
| |
| result |
| }; |
| |
| if changed { |
| // Note that there's no need to remove the attribute here if |
| // the declaration block is empty[1], and if `attr` is |
| // `None` it means that it necessarily didn't change, so no |
| // need to go through all the set_attribute machinery. |
| // |
| // [1]: https://github.com/whatwg/html/issues/2306 |
| if let Some(pdb) = attr { |
| let guard = shared_lock.read(); |
| let mut serialization = String::new(); |
| pdb.read_with(&guard).to_css(&mut serialization).unwrap(); |
| el.set_attribute( |
| &local_name!("style"), |
| AttrValue::Declaration(serialization, pdb), |
| can_gc, |
| ); |
| } |
| } else { |
| // Remember to put it back. |
| *el.style_attribute().borrow_mut() = attr; |
| } |
| |
| result |
| }, |
| CSSStyleOwner::CSSRule(ref rule, ref pdb) => { |
| rule.parent_stylesheet().will_modify(); |
| let result = { |
| let mut guard = rule.shared_lock().write(); |
| f(&mut *pdb.borrow().write_with(&mut guard), &mut changed) |
| }; |
| if changed { |
| rule.parent_stylesheet().notify_invalidations(); |
| } |
| result |
| }, |
| } |
| } |
| |
| fn with_block<F, R>(&self, f: F) -> R |
| where |
| F: FnOnce(&PropertyDeclarationBlock) -> R, |
| { |
| match *self { |
| CSSStyleOwner::Null => { |
| unreachable!("Should never call with_block for CSStyleOwner::Null") |
| }, |
| CSSStyleOwner::Element(ref el) => match *el.style_attribute().borrow() { |
| Some(ref pdb) => { |
| let document = el.owner_document(); |
| let guard = document.style_shared_lock().read(); |
| f(pdb.read_with(&guard)) |
| }, |
| None => { |
| let pdb = PropertyDeclarationBlock::new(); |
| f(&pdb) |
| }, |
| }, |
| CSSStyleOwner::CSSRule(ref rule, ref pdb) => { |
| let guard = rule.shared_lock().read(); |
| f(pdb.borrow().read_with(&guard)) |
| }, |
| } |
| } |
| |
| fn window(&self) -> DomRoot<Window> { |
| match *self { |
| CSSStyleOwner::Null => { |
| unreachable!("Should never try to access window of CSStyleOwner::Null") |
| }, |
| CSSStyleOwner::Element(ref el) => el.owner_window(), |
| CSSStyleOwner::CSSRule(ref rule, _) => DomRoot::from_ref(rule.global().as_window()), |
| } |
| } |
| |
| fn base_url(&self) -> ServoUrl { |
| match *self { |
| CSSStyleOwner::Null => { |
| unreachable!("Should never try to access base URL of CSStyleOwner::Null") |
| }, |
| CSSStyleOwner::Element(ref el) => el.owner_document().base_url(), |
| CSSStyleOwner::CSSRule(ref rule, _) => ServoUrl::from( |
| rule.parent_stylesheet() |
| .style_stylesheet() |
| .contents |
| .url_data |
| .read() |
| .0 |
| .clone(), |
| ) |
| .clone(), |
| } |
| } |
| } |
| |
| #[derive(MallocSizeOf, PartialEq)] |
| pub(crate) enum CSSModificationAccess { |
| ReadWrite, |
| Readonly, |
| } |
| |
| macro_rules! css_properties( |
| ( $([$getter:ident, $setter:ident, $id:expr],)* ) => ( |
| $( |
| fn $getter(&self) -> DOMString { |
| debug_assert!( |
| $id.enabled_for_all_content(), |
| "Someone forgot a #[Pref] annotation" |
| ); |
| self.get_property_value($id) |
| } |
| fn $setter(&self, value: DOMString) -> ErrorResult { |
| debug_assert!( |
| $id.enabled_for_all_content(), |
| "Someone forgot a #[Pref] annotation" |
| ); |
| self.set_property($id, value, DOMString::new(), CanGc::note()) |
| } |
| )* |
| ); |
| ); |
| |
| fn remove_property(decls: &mut PropertyDeclarationBlock, id: &PropertyId) -> bool { |
| let first_declaration = decls.first_declaration_to_remove(id); |
| let first_declaration = match first_declaration { |
| Some(i) => i, |
| None => return false, |
| }; |
| decls.remove_property(id, first_declaration); |
| true |
| } |
| |
| impl CSSStyleDeclaration { |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| pub(crate) fn new_inherited( |
| owner: CSSStyleOwner, |
| pseudo: Option<PseudoElement>, |
| modification_access: CSSModificationAccess, |
| ) -> CSSStyleDeclaration { |
| // If creating a CSSStyleDeclaration with CSSSStyleOwner::Null, this should always |
| // be in read-only mode. |
| assert!( |
| !matches!(owner, CSSStyleOwner::Null) || |
| modification_access == CSSModificationAccess::Readonly |
| ); |
| |
| CSSStyleDeclaration { |
| reflector_: Reflector::new(), |
| owner, |
| readonly: modification_access == CSSModificationAccess::Readonly, |
| pseudo, |
| } |
| } |
| |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| pub(crate) fn new( |
| global: &Window, |
| owner: CSSStyleOwner, |
| pseudo: Option<PseudoElement>, |
| modification_access: CSSModificationAccess, |
| can_gc: CanGc, |
| ) -> DomRoot<CSSStyleDeclaration> { |
| reflect_dom_object( |
| Box::new(CSSStyleDeclaration::new_inherited( |
| owner, |
| pseudo, |
| modification_access, |
| )), |
| global, |
| can_gc, |
| ) |
| } |
| |
| pub(crate) fn update_property_declaration_block( |
| &self, |
| pdb: &Arc<Locked<PropertyDeclarationBlock>>, |
| ) { |
| if let CSSStyleOwner::CSSRule(_, pdb_cell) = &self.owner { |
| *pdb_cell.borrow_mut() = pdb.clone(); |
| } else { |
| panic!("update_rule called on CSSStyleDeclaration with a Element owner"); |
| } |
| } |
| |
| fn get_computed_style(&self, property: PropertyId) -> DOMString { |
| match self.owner { |
| CSSStyleOwner::CSSRule(..) => { |
| panic!("get_computed_style called on CSSStyleDeclaration with a CSSRule owner") |
| }, |
| CSSStyleOwner::Element(ref el) => { |
| let node = el.upcast::<Node>(); |
| if !node.is_connected() { |
| return DOMString::new(); |
| } |
| let addr = node.to_trusted_node_address(); |
| node.owner_window() |
| .resolved_style_query(addr, self.pseudo, property) |
| }, |
| CSSStyleOwner::Null => DOMString::new(), |
| } |
| } |
| |
| fn get_property_value(&self, id: PropertyId) -> DOMString { |
| if matches!(self.owner, CSSStyleOwner::Null) { |
| return DOMString::new(); |
| } |
| |
| if self.readonly { |
| // Readonly style declarations are used for getComputedStyle. |
| return self.get_computed_style(id); |
| } |
| |
| let mut string = String::new(); |
| |
| self.owner.with_block(|pdb| { |
| pdb.property_value_to_css(&id, &mut string).unwrap(); |
| }); |
| |
| DOMString::from(string) |
| } |
| |
| /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty> |
| fn set_property( |
| &self, |
| id: PropertyId, |
| value: DOMString, |
| priority: DOMString, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| self.set_property_inner( |
| PotentiallyParsedPropertyId::Parsed(id), |
| value, |
| priority, |
| can_gc, |
| ) |
| } |
| |
| /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty> |
| /// |
| /// This function receives a `PotentiallyParsedPropertyId` instead of a `DOMString` in case |
| /// the caller already has a parsed property ID. |
| fn set_property_inner( |
| &self, |
| id: PotentiallyParsedPropertyId, |
| value: DOMString, |
| priority: DOMString, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| // Step 1. If the readonly flag is set, then throw a NoModificationAllowedError exception. |
| if self.readonly { |
| return Err(Error::NoModificationAllowed); |
| } |
| |
| let id = match id { |
| PotentiallyParsedPropertyId::Parsed(id) => { |
| if !id.enabled_for_all_content() { |
| return Ok(()); |
| } |
| |
| id |
| }, |
| PotentiallyParsedPropertyId::NotParsed(unparsed) => { |
| match PropertyId::parse_enabled_for_all_content(&unparsed) { |
| Ok(id) => id, |
| Err(..) => return Ok(()), |
| } |
| }, |
| }; |
| |
| self.owner.mutate_associated_block( |
| |pdb, changed| { |
| // Step 3. If value is the empty string, invoke removeProperty() |
| // with property as argument and return. |
| if value.is_empty() { |
| *changed = remove_property(pdb, &id); |
| return Ok(()); |
| } |
| |
| // Step 4. If priority is not the empty string and is not an ASCII case-insensitive |
| // match for the string "important", then return. |
| let importance = match &*priority { |
| "" => Importance::Normal, |
| p if p.eq_ignore_ascii_case("important") => Importance::Important, |
| _ => { |
| *changed = false; |
| return Ok(()); |
| }, |
| }; |
| |
| // Step 5 |
| let window = self.owner.window(); |
| let quirks_mode = window.Document().quirks_mode(); |
| let mut declarations = SourcePropertyDeclaration::default(); |
| let result = parse_one_declaration_into( |
| &mut declarations, |
| id, |
| &value, |
| Origin::Author, |
| &UrlExtraData(self.owner.base_url().get_arc()), |
| window.css_error_reporter(), |
| ParsingMode::DEFAULT, |
| quirks_mode, |
| CssRuleType::Style, |
| ); |
| |
| // Step 6 |
| match result { |
| Ok(()) => {}, |
| Err(_) => { |
| *changed = false; |
| return Ok(()); |
| }, |
| } |
| |
| let mut updates = Default::default(); |
| *changed = pdb.prepare_for_update(&declarations, importance, &mut updates); |
| |
| if !*changed { |
| return Ok(()); |
| } |
| |
| // Step 7 |
| // Step 8 |
| pdb.update(declarations.drain(), importance, &mut updates); |
| |
| Ok(()) |
| }, |
| can_gc, |
| ) |
| } |
| } |
| |
| pub(crate) static ENABLED_LONGHAND_PROPERTIES: LazyLock<Vec<LonghandId>> = LazyLock::new(|| { |
| // The 'all' shorthand contains all the enabled longhands with 2 exceptions: |
| // 'direction' and 'unicode-bidi', so these must be added afterward. |
| let mut enabled_longhands: Vec<LonghandId> = ShorthandId::All.longhands().collect(); |
| if PropertyId::NonCustom(LonghandId::Direction.into()).enabled_for_all_content() { |
| enabled_longhands.push(LonghandId::Direction); |
| } |
| if PropertyId::NonCustom(LonghandId::UnicodeBidi.into()).enabled_for_all_content() { |
| enabled_longhands.push(LonghandId::UnicodeBidi); |
| } |
| |
| // Sort lexicographically, but with vendor-prefixed properties after standard ones. |
| enabled_longhands.sort_unstable_by(|a, b| { |
| let a = a.name(); |
| let b = b.name(); |
| let is_a_vendor_prefixed = a.starts_with('-'); |
| let is_b_vendor_prefixed = b.starts_with('-'); |
| if is_a_vendor_prefixed == is_b_vendor_prefixed { |
| a.partial_cmp(b).unwrap() |
| } else if is_b_vendor_prefixed { |
| Ordering::Less |
| } else { |
| Ordering::Greater |
| } |
| }); |
| enabled_longhands |
| }); |
| |
| enum PotentiallyParsedPropertyId { |
| Parsed(PropertyId), |
| NotParsed(DOMString), |
| } |
| |
| impl CSSStyleDeclarationMethods<crate::DomTypeHolder> for CSSStyleDeclaration { |
| // https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-length |
| fn Length(&self) -> u32 { |
| if matches!(self.owner, CSSStyleOwner::Null) { |
| return 0; |
| } |
| |
| if self.readonly { |
| // Readonly style declarations are used for getComputedStyle. |
| // TODO: include custom properties whose computed value is not the guaranteed-invalid value. |
| return ENABLED_LONGHAND_PROPERTIES.len() as u32; |
| } |
| self.owner.with_block(|pdb| pdb.declarations().len() as u32) |
| } |
| |
| // https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-item |
| fn Item(&self, index: u32) -> DOMString { |
| self.IndexedGetter(index).unwrap_or_default() |
| } |
| |
| // https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-getpropertyvalue |
| fn GetPropertyValue(&self, property: DOMString) -> DOMString { |
| let id = match PropertyId::parse_enabled_for_all_content(&property) { |
| Ok(id) => id, |
| Err(..) => return DOMString::new(), |
| }; |
| self.get_property_value(id) |
| } |
| |
| // https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-getpropertypriority |
| fn GetPropertyPriority(&self, property: DOMString) -> DOMString { |
| if self.readonly { |
| // Readonly style declarations are used for getComputedStyle. |
| return DOMString::new(); |
| } |
| let id = match PropertyId::parse_enabled_for_all_content(&property) { |
| Ok(id) => id, |
| Err(..) => return DOMString::new(), |
| }; |
| |
| self.owner.with_block(|pdb| { |
| if pdb.property_priority(&id).important() { |
| DOMString::from("important") |
| } else { |
| // Step 4 |
| DOMString::new() |
| } |
| }) |
| } |
| |
| /// <https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-setproperty> |
| fn SetProperty( |
| &self, |
| property: DOMString, |
| value: DOMString, |
| priority: DOMString, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| self.set_property_inner( |
| PotentiallyParsedPropertyId::NotParsed(property), |
| value, |
| priority, |
| can_gc, |
| ) |
| } |
| |
| // https://dev.w3.org/csswg/cssom/#dom-cssstyledeclaration-removeproperty |
| fn RemoveProperty(&self, property: DOMString, can_gc: CanGc) -> Fallible<DOMString> { |
| // Step 1 |
| if self.readonly { |
| return Err(Error::NoModificationAllowed); |
| } |
| |
| let id = match PropertyId::parse_enabled_for_all_content(&property) { |
| Ok(id) => id, |
| Err(..) => return Ok(DOMString::new()), |
| }; |
| |
| let mut string = String::new(); |
| self.owner.mutate_associated_block( |
| |pdb, changed| { |
| pdb.property_value_to_css(&id, &mut string).unwrap(); |
| *changed = remove_property(pdb, &id); |
| }, |
| can_gc, |
| ); |
| |
| // Step 6 |
| Ok(DOMString::from(string)) |
| } |
| |
| /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat> |
| fn CssFloat(&self) -> DOMString { |
| self.get_property_value(PropertyId::NonCustom(LonghandId::Float.into())) |
| } |
| |
| /// <https://drafts.csswg.org/cssom/#dom-cssstyleproperties-cssfloat> |
| fn SetCssFloat(&self, value: DOMString, can_gc: CanGc) -> ErrorResult { |
| self.set_property( |
| PropertyId::NonCustom(LonghandId::Float.into()), |
| value, |
| DOMString::new(), |
| can_gc, |
| ) |
| } |
| |
| // https://dev.w3.org/csswg/cssom/#the-cssstyledeclaration-interface |
| fn IndexedGetter(&self, index: u32) -> Option<DOMString> { |
| if matches!(self.owner, CSSStyleOwner::Null) { |
| return None; |
| } |
| if self.readonly { |
| // Readonly style declarations are used for getComputedStyle. |
| // TODO: include custom properties whose computed value is not the guaranteed-invalid value. |
| let longhand = ENABLED_LONGHAND_PROPERTIES.get(index as usize)?; |
| return Some(DOMString::from(longhand.name())); |
| } |
| self.owner.with_block(|pdb| { |
| let declaration = pdb.declarations().get(index as usize)?; |
| Some(DOMString::from(declaration.id().name())) |
| }) |
| } |
| |
| // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext |
| fn CssText(&self) -> DOMString { |
| if self.readonly { |
| // Readonly style declarations are used for getComputedStyle. |
| return DOMString::new(); |
| } |
| self.owner.with_block(|pdb| { |
| let mut serialization = String::new(); |
| pdb.to_css(&mut serialization).unwrap(); |
| DOMString::from(serialization) |
| }) |
| } |
| |
| // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext |
| fn SetCssText(&self, value: DOMString, can_gc: CanGc) -> ErrorResult { |
| let window = self.owner.window(); |
| |
| // Step 1 |
| if self.readonly { |
| return Err(Error::NoModificationAllowed); |
| } |
| |
| let quirks_mode = window.Document().quirks_mode(); |
| self.owner.mutate_associated_block( |
| |pdb, _changed| { |
| // Step 3 |
| *pdb = parse_style_attribute( |
| &value, |
| &UrlExtraData(self.owner.base_url().get_arc()), |
| window.css_error_reporter(), |
| quirks_mode, |
| CssRuleType::Style, |
| ); |
| }, |
| can_gc, |
| ); |
| |
| Ok(()) |
| } |
| |
| // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-_camel_cased_attribute |
| style::css_properties_accessors!(css_properties); |
| } |