diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /servo/components/style/custom_properties.rs | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/custom_properties.rs')
-rw-r--r-- | servo/components/style/custom_properties.rs | 1185 |
1 files changed, 1185 insertions, 0 deletions
diff --git a/servo/components/style/custom_properties.rs b/servo/components/style/custom_properties.rs new file mode 100644 index 0000000000..5e37963186 --- /dev/null +++ b/servo/components/style/custom_properties.rs @@ -0,0 +1,1185 @@ +/* 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/. */ + +//! Support for [custom properties for cascading variables][custom]. +//! +//! [custom]: https://drafts.csswg.org/css-variables/ + +use crate::applicable_declarations::CascadePriority; +use crate::media_queries::Device; +use crate::properties::{CSSWideKeyword, CustomDeclaration, CustomDeclarationValue}; +use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet, PrecomputedHasher}; +use crate::Atom; +use cssparser::{ + CowRcStr, Delimiter, Parser, ParserInput, SourcePosition, Token, TokenSerializationType, +}; +use indexmap::IndexMap; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::cmp; +use std::collections::hash_map::Entry; +use std::fmt::{self, Write}; +use std::hash::BuildHasherDefault; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// The environment from which to get `env` function values. +/// +/// TODO(emilio): If this becomes a bit more complex we should probably move it +/// to the `media_queries` module, or something. +#[derive(Debug, MallocSizeOf)] +pub struct CssEnvironment; + +type EnvironmentEvaluator = fn(device: &Device) -> VariableValue; + +struct EnvironmentVariable { + name: Atom, + evaluator: EnvironmentEvaluator, +} + +macro_rules! make_variable { + ($name:expr, $evaluator:expr) => {{ + EnvironmentVariable { + name: $name, + evaluator: $evaluator, + } + }}; +} + +fn get_safearea_inset_top(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().top) +} + +fn get_safearea_inset_bottom(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().bottom) +} + +fn get_safearea_inset_left(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().left) +} + +fn get_safearea_inset_right(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().right) +} + +fn get_content_preferred_color_scheme(device: &Device) -> VariableValue { + use crate::gecko::media_features::PrefersColorScheme; + let prefers_color_scheme = unsafe { + crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme( + device.document(), + /* use_content = */ true, + ) + }; + VariableValue::ident(match prefers_color_scheme { + PrefersColorScheme::Light => "light", + PrefersColorScheme::Dark => "dark", + }) +} + +fn get_scrollbar_inline_size(device: &Device) -> VariableValue { + VariableValue::pixels(device.scrollbar_inline_size().px()) +} + +static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [ + make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top), + make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom), + make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left), + make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right), +]; + +macro_rules! lnf_int { + ($id:ident) => { + unsafe { + crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt( + crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32, + ) + } + }; +} + +macro_rules! lnf_int_variable { + ($atom:expr, $id:ident, $ctor:ident) => {{ + fn __eval(_: &Device) -> VariableValue { + VariableValue::$ctor(lnf_int!($id)) + } + make_variable!($atom, __eval) + }}; +} + +static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 6] = [ + lnf_int_variable!( + atom!("-moz-gtk-csd-titlebar-radius"), + TitlebarRadius, + int_pixels + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-close-button-position"), + GTKCSDCloseButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-minimize-button-position"), + GTKCSDMinimizeButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-maximize-button-position"), + GTKCSDMaximizeButtonPosition, + integer + ), + make_variable!( + atom!("-moz-content-preferred-color-scheme"), + get_content_preferred_color_scheme + ), + make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size), +]; + +impl CssEnvironment { + #[inline] + fn get(&self, name: &Atom, device: &Device) -> Option<VariableValue> { + if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) { + return Some((var.evaluator)(device)); + } + if !device.chrome_rules_enabled_for_document() { + return None; + } + let var = CHROME_ENVIRONMENT_VARIABLES + .iter() + .find(|var| var.name == *name)?; + Some((var.evaluator)(device)) + } +} + +/// A custom property name is just an `Atom`. +/// +/// Note that this does not include the `--` prefix +pub type Name = Atom; + +/// Parse a custom property name. +/// +/// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name> +pub fn parse_name(s: &str) -> Result<&str, ()> { + if s.starts_with("--") && s.len() > 2 { + Ok(&s[2..]) + } else { + Err(()) + } +} + +/// A value for a custom property is just a set of tokens. +/// +/// We preserve the original CSS for serialization, and also the variable +/// references to other custom property names. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct VariableValue { + css: String, + + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + + /// Whether a variable value has a reference to an environment variable. + /// + /// If this is the case, we need to perform variable substitution on the + /// value. + references_environment: bool, + + /// Custom property names in var() functions. + references: Box<[Name]>, +} + +impl ToCss for SpecifiedValue { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.css) + } +} + +/// A map from CSS variable names to CSS variable computed values, used for +/// resolving. +/// +/// A consistent ordering is required for CSSDeclaration objects in the +/// DOM. CSSDeclarations expose property names as indexed properties, which +/// need to be stable. So we keep an array of property names which order is +/// determined on the order that they are added to the name-value map. +/// +/// The variable values are guaranteed to not have references to other +/// properties. +pub type CustomPropertiesMap = + IndexMap<Name, Arc<VariableValue>, BuildHasherDefault<PrecomputedHasher>>; + +/// Both specified and computed values are VariableValues, the difference is +/// whether var() functions are expanded. +pub type SpecifiedValue = VariableValue; +/// Both specified and computed values are VariableValues, the difference is +/// whether var() functions are expanded. +pub type ComputedValue = VariableValue; + +/// A struct holding information about the external references to that a custom +/// property value may have. +#[derive(Default)] +struct VarOrEnvReferences { + custom_property_references: PrecomputedHashSet<Name>, + references_environment: bool, +} + +impl VariableValue { + fn empty() -> Self { + Self { + css: String::new(), + last_token_type: TokenSerializationType::nothing(), + first_token_type: TokenSerializationType::nothing(), + references: Default::default(), + references_environment: false, + } + } + + fn push<'i>( + &mut self, + input: &Parser<'i, '_>, + css: &str, + css_first_token_type: TokenSerializationType, + css_last_token_type: TokenSerializationType, + ) -> Result<(), ParseError<'i>> { + /// Prevent values from getting terribly big since you can use custom + /// properties exponentially. + /// + /// This number (2MB) is somewhat arbitrary, but silly enough that no + /// reasonable page should hit it. We could limit by number of total + /// substitutions, but that was very easy to work around in practice + /// (just choose a larger initial value and boom). + const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024; + + if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // This happens e.g. between two subsequent var() functions: + // `var(--a)var(--b)`. + // + // In that case, css_*_token_type is nonsensical. + if css.is_empty() { + return Ok(()); + } + + self.first_token_type.set_if_nothing(css_first_token_type); + // If self.first_token_type was nothing, + // self.last_token_type is also nothing and this will be false: + if self + .last_token_type + .needs_separator_when_before(css_first_token_type) + { + self.css.push_str("/**/") + } + self.css.push_str(css); + self.last_token_type = css_last_token_type; + Ok(()) + } + + fn push_from<'i>( + &mut self, + input: &Parser<'i, '_>, + position: (SourcePosition, TokenSerializationType), + last_token_type: TokenSerializationType, + ) -> Result<(), ParseError<'i>> { + self.push( + input, + input.slice_from(position.0), + position.1, + last_token_type, + ) + } + + fn push_variable<'i>( + &mut self, + input: &Parser<'i, '_>, + variable: &ComputedValue, + ) -> Result<(), ParseError<'i>> { + debug_assert!(variable.references.is_empty()); + self.push( + input, + &variable.css, + variable.first_token_type, + variable.last_token_type, + ) + } + + /// Parse a custom property value. + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Arc<Self>, ParseError<'i>> { + let mut references = VarOrEnvReferences::default(); + + let (first_token_type, css, last_token_type) = + parse_self_contained_declaration_value(input, Some(&mut references))?; + + let custom_property_references = references + .custom_property_references + .into_iter() + .collect::<Vec<_>>() + .into_boxed_slice(); + + let mut css = css.into_owned(); + css.shrink_to_fit(); + + Ok(Arc::new(VariableValue { + css, + first_token_type, + last_token_type, + references: custom_property_references, + references_environment: references.references_environment, + })) + } + + /// Create VariableValue from an int. + fn integer(number: i32) -> Self { + Self::from_token(Token::Number { + has_sign: false, + value: number as f32, + int_value: Some(number), + }) + } + + /// Create VariableValue from an int. + fn ident(ident: &'static str) -> Self { + Self::from_token(Token::Ident(ident.into())) + } + + /// Create VariableValue from a float amount of CSS pixels. + fn pixels(number: f32) -> Self { + // FIXME (https://github.com/servo/rust-cssparser/issues/266): + // No way to get TokenSerializationType::Dimension without creating + // Token object. + Self::from_token(Token::Dimension { + has_sign: false, + value: number, + int_value: None, + unit: CowRcStr::from("px"), + }) + } + + /// Create VariableValue from an integer amount of CSS pixels. + fn int_pixels(number: i32) -> Self { + Self::from_token(Token::Dimension { + has_sign: false, + value: number as f32, + int_value: Some(number), + unit: CowRcStr::from("px"), + }) + } + + fn from_token(token: Token) -> Self { + let token_type = token.serialization_type(); + let mut css = token.to_css_string(); + css.shrink_to_fit(); + + VariableValue { + css, + first_token_type: token_type, + last_token_type: token_type, + references: Default::default(), + references_environment: false, + } + } +} + +/// Parse the value of a non-custom property that contains `var()` references. +pub fn parse_non_custom_with_var<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<(TokenSerializationType, Cow<'i, str>), ParseError<'i>> { + let (first_token_type, css, _) = parse_self_contained_declaration_value(input, None)?; + Ok((first_token_type, css)) +} + +fn parse_self_contained_declaration_value<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(TokenSerializationType, Cow<'i, str>, TokenSerializationType), ParseError<'i>> { + let start_position = input.position(); + let mut missing_closing_characters = String::new(); + let (first, last) = + parse_declaration_value(input, references, &mut missing_closing_characters)?; + let mut css: Cow<str> = input.slice_from(start_position).into(); + if !missing_closing_characters.is_empty() { + // Unescaped backslash at EOF in a quoted string is ignored. + if css.ends_with("\\") && matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'') { + css.to_mut().pop(); + } + css.to_mut().push_str(&missing_closing_characters); + } + Ok((first, css, last)) +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value> +fn parse_declaration_value<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + parse_declaration_value_block(input, references, missing_closing_characters) + }) +} + +/// Like parse_declaration_value, but accept `!` and `;` since they are only +/// invalid at the top level +fn parse_declaration_value_block<'i, 't>( + input: &mut Parser<'i, 't>, + mut references: Option<&mut VarOrEnvReferences>, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + input.skip_whitespace(); + let mut token_start = input.position(); + let mut token = match input.next_including_whitespace_and_comments() { + Ok(token) => token, + Err(_) => { + return Ok(( + TokenSerializationType::nothing(), + TokenSerializationType::nothing(), + )); + }, + }; + let first_token_type = token.serialization_type(); + loop { + macro_rules! nested { + () => { + input.parse_nested_block(|input| { + parse_declaration_value_block( + input, + references.as_mut().map(|r| &mut **r), + missing_closing_characters, + ) + })? + }; + } + macro_rules! check_closed { + ($closing:expr) => { + if !input.slice_from(token_start).ends_with($closing) { + missing_closing_characters.push_str($closing) + } + }; + } + let last_token_type = match *token { + Token::Comment(_) => { + let serialization_type = token.serialization_type(); + let token_slice = input.slice_from(token_start); + if !token_slice.ends_with("*/") { + missing_closing_characters.push_str(if token_slice.ends_with('*') { + "/" + } else { + "*/" + }) + } + serialization_type + }, + Token::BadUrl(ref u) => { + let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock(u.clone()); + return Err(input.new_custom_error(e)); + }, + Token::BadString(ref s) => { + let e = StyleParseErrorKind::BadStringInDeclarationValueBlock(s.clone()); + return Err(input.new_custom_error(e)); + }, + Token::CloseParenthesis => { + let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::CloseSquareBracket => { + let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::CloseCurlyBracket => { + let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::Function(ref name) => { + if name.eq_ignore_ascii_case("var") { + let args_start = input.state(); + input.parse_nested_block(|input| { + parse_var_function(input, references.as_mut().map(|r| &mut **r)) + })?; + input.reset(&args_start); + } else if name.eq_ignore_ascii_case("env") { + let args_start = input.state(); + input.parse_nested_block(|input| { + parse_env_function(input, references.as_mut().map(|r| &mut **r)) + })?; + input.reset(&args_start); + } + nested!(); + check_closed!(")"); + Token::CloseParenthesis.serialization_type() + }, + Token::ParenthesisBlock => { + nested!(); + check_closed!(")"); + Token::CloseParenthesis.serialization_type() + }, + Token::CurlyBracketBlock => { + nested!(); + check_closed!("}"); + Token::CloseCurlyBracket.serialization_type() + }, + Token::SquareBracketBlock => { + nested!(); + check_closed!("]"); + Token::CloseSquareBracket.serialization_type() + }, + Token::QuotedString(_) => { + let serialization_type = token.serialization_type(); + let token_slice = input.slice_from(token_start); + let quote = &token_slice[..1]; + debug_assert!(matches!(quote, "\"" | "'")); + if !(token_slice.ends_with(quote) && token_slice.len() > 1) { + missing_closing_characters.push_str(quote) + } + serialization_type + }, + Token::Ident(ref value) | + Token::AtKeyword(ref value) | + Token::Hash(ref value) | + Token::IDHash(ref value) | + Token::UnquotedUrl(ref value) | + Token::Dimension { + unit: ref value, .. + } => { + let serialization_type = token.serialization_type(); + let is_unquoted_url = matches!(token, Token::UnquotedUrl(_)); + if value.ends_with("�") && input.slice_from(token_start).ends_with("\\") { + // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD + // Check the value in case the final backslash was itself escaped. + // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD. + // (Unescaped U+FFFD would also work, but removing the backslash is annoying.) + missing_closing_characters.push_str("�") + } + if is_unquoted_url { + check_closed!(")"); + } + serialization_type + }, + _ => token.serialization_type(), + }; + + token_start = input.position(); + token = match input.next_including_whitespace_and_comments() { + Ok(token) => token, + Err(..) => return Ok((first_token_type, last_token_type)), + }; + } +} + +fn parse_fallback<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + // Exclude `!` and `;` at the top level + // https://drafts.csswg.org/css-syntax/#typedef-declaration-value + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + // Skip until the end. + while input.next_including_whitespace_and_comments().is_ok() {} + Ok(()) + }) +} + +// If the var function is valid, return Ok((custom_property_name, fallback)) +fn parse_var_function<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(), ParseError<'i>> { + let name = input.expect_ident_cloned()?; + let name = parse_name(&name).map_err(|()| { + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())) + })?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + parse_fallback(input)?; + } + if let Some(refs) = references { + refs.custom_property_references.insert(Atom::from(name)); + } + Ok(()) +} + +fn parse_env_function<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(), ParseError<'i>> { + // TODO(emilio): This should be <custom-ident> per spec, but no other + // browser does that, see https://github.com/w3c/csswg-drafts/issues/3262. + input.expect_ident()?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + parse_fallback(input)?; + } + if let Some(references) = references { + references.references_environment = true; + } + Ok(()) +} + +/// A struct that takes care of encapsulating the cascade process for custom +/// properties. +pub struct CustomPropertiesBuilder<'a> { + seen: PrecomputedHashSet<&'a Name>, + may_have_cycles: bool, + custom_properties: Option<CustomPropertiesMap>, + inherited: Option<&'a Arc<CustomPropertiesMap>>, + reverted: PrecomputedHashMap<&'a Name, (CascadePriority, bool)>, + device: &'a Device, +} + +impl<'a> CustomPropertiesBuilder<'a> { + /// Create a new builder, inheriting from a given custom properties map. + pub fn new(inherited: Option<&'a Arc<CustomPropertiesMap>>, device: &'a Device) -> Self { + Self { + seen: PrecomputedHashSet::default(), + reverted: Default::default(), + may_have_cycles: false, + custom_properties: None, + inherited, + device, + } + } + + /// Cascade a given custom property declaration. + pub fn cascade(&mut self, declaration: &'a CustomDeclaration, priority: CascadePriority) { + let CustomDeclaration { + ref name, + ref value, + } = *declaration; + + if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&name) { + if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) { + return; + } + } + + let was_already_present = !self.seen.insert(name); + if was_already_present { + return; + } + + if !self.value_may_affect_style(name, value) { + return; + } + + if self.custom_properties.is_none() { + self.custom_properties = Some(match self.inherited { + Some(inherited) => (**inherited).clone(), + None => CustomPropertiesMap::default(), + }); + } + + let map = self.custom_properties.as_mut().unwrap(); + match *value { + CustomDeclarationValue::Value(ref unparsed_value) => { + let has_references = !unparsed_value.references.is_empty(); + self.may_have_cycles |= has_references; + + // If the variable value has no references and it has an + // environment variable here, perform substitution here instead + // of forcing a full traversal in `substitute_all` afterwards. + let value = if !has_references && unparsed_value.references_environment { + let result = substitute_references_in_value(unparsed_value, &map, &self.device); + match result { + Ok(new_value) => new_value, + Err(..) => { + map.remove(name); + return; + }, + } + } else { + (*unparsed_value).clone() + }; + map.insert(name.clone(), value); + }, + CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { + CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => { + let origin_revert = keyword == CSSWideKeyword::Revert; + self.seen.remove(name); + self.reverted.insert(name, (priority, origin_revert)); + }, + CSSWideKeyword::Initial => { + map.remove(name); + }, + // handled in value_may_affect_style + CSSWideKeyword::Unset | CSSWideKeyword::Inherit => unreachable!(), + }, + } + } + + fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool { + match *value { + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) | + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => { + // Custom properties are inherited by default. So + // explicit 'inherit' or 'unset' means we can just use + // any existing value in the inherited CustomPropertiesMap. + return false; + }, + _ => {}, + } + + let existing_value = self + .custom_properties + .as_ref() + .and_then(|m| m.get(name)) + .or_else(|| self.inherited.and_then(|m| m.get(name))); + + match (existing_value, value) { + (None, &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial)) => { + // The initial value of a custom property is the same as it + // not existing in the map. + return false; + }, + (Some(existing_value), &CustomDeclarationValue::Value(ref value)) => { + // Don't bother overwriting an existing inherited value with + // the same specified value. + if existing_value == value { + return false; + } + }, + _ => {}, + } + + true + } + + fn inherited_properties_match(&self, map: &CustomPropertiesMap) -> bool { + let inherited = match self.inherited { + Some(inherited) => inherited, + None => return false, + }; + if inherited.len() != map.len() { + return false; + } + for name in self.seen.iter() { + if inherited.get(*name) != map.get(*name) { + return false; + } + } + true + } + + /// Returns the final map of applicable custom properties. + /// + /// If there was any specified property, we've created a new map and now we + /// need to remove any potential cycles, and wrap it in an arc. + /// + /// Otherwise, just use the inherited custom properties map. + pub fn build(mut self) -> Option<Arc<CustomPropertiesMap>> { + let mut map = match self.custom_properties.take() { + Some(m) => m, + None => return self.inherited.cloned(), + }; + + if self.may_have_cycles { + substitute_all(&mut map, &self.seen, self.device); + } + + // Some pages apply a lot of redundant custom properties, see e.g. + // bug 1758974 comment 5. Try to detect the case where the values + // haven't really changed, and save some memory by reusing the inherited + // map in that case. + if self.inherited_properties_match(&map) { + return self.inherited.cloned(); + } + + map.shrink_to_fit(); + Some(Arc::new(map)) + } +} + +/// Resolve all custom properties to either substituted, invalid, or unset +/// (meaning we should use the inherited value). +/// +/// It does cycle dependencies removal at the same time as substitution. +fn substitute_all( + custom_properties_map: &mut CustomPropertiesMap, + seen: &PrecomputedHashSet<&Name>, + device: &Device, +) { + // The cycle dependencies removal in this function is a variant + // of Tarjan's algorithm. It is mostly based on the pseudo-code + // listed in + // https://en.wikipedia.org/w/index.php? + // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495 + + /// Struct recording necessary information for each variable. + #[derive(Debug)] + struct VarInfo { + /// The name of the variable. It will be taken to save addref + /// when the corresponding variable is popped from the stack. + /// This also serves as a mark for whether the variable is + /// currently in the stack below. + name: Option<Name>, + /// If the variable is in a dependency cycle, lowlink represents + /// a smaller index which corresponds to a variable in the same + /// strong connected component, which is known to be accessible + /// from this variable. It is not necessarily the root, though. + lowlink: usize, + } + /// Context struct for traversing the variable graph, so that we can + /// avoid referencing all the fields multiple times. + #[derive(Debug)] + struct Context<'a> { + /// Number of variables visited. This is used as the order index + /// when we visit a new unresolved variable. + count: usize, + /// The map from custom property name to its order index. + index_map: PrecomputedHashMap<Name, usize>, + /// Information of each variable indexed by the order index. + var_info: SmallVec<[VarInfo; 5]>, + /// The stack of order index of visited variables. It contains + /// all unfinished strong connected components. + stack: SmallVec<[usize; 5]>, + map: &'a mut CustomPropertiesMap, + /// To resolve the environment to substitute `env()` variables. + device: &'a Device, + } + + /// This function combines the traversal for cycle removal and value + /// substitution. It returns either a signal None if this variable + /// has been fully resolved (to either having no reference or being + /// marked invalid), or the order index for the given name. + /// + /// When it returns, the variable corresponds to the name would be + /// in one of the following states: + /// * It is still in context.stack, which means it is part of an + /// potentially incomplete dependency circle. + /// * It has been removed from the map. It can be either that the + /// substitution failed, or it is inside a dependency circle. + /// When this function removes a variable from the map because + /// of dependency circle, it would put all variables in the same + /// strong connected component to the set together. + /// * It doesn't have any reference, because either this variable + /// doesn't have reference at all in specified value, or it has + /// been completely resolved. + /// * There is no such variable at all. + fn traverse<'a>(name: &Name, context: &mut Context<'a>) -> Option<usize> { + // Some shortcut checks. + let (name, value) = { + let value = context.map.get(name)?; + + // Nothing to resolve. + if value.references.is_empty() { + debug_assert!( + !value.references_environment, + "Should've been handled earlier" + ); + return None; + } + + // Whether this variable has been visited in this traversal. + let key; + match context.index_map.entry(name.clone()) { + Entry::Occupied(entry) => { + return Some(*entry.get()); + }, + Entry::Vacant(entry) => { + key = entry.key().clone(); + entry.insert(context.count); + }, + } + + // Hold a strong reference to the value so that we don't + // need to keep reference to context.map. + (key, value.clone()) + }; + + // Add new entry to the information table. + let index = context.count; + context.count += 1; + debug_assert_eq!(index, context.var_info.len()); + context.var_info.push(VarInfo { + name: Some(name), + lowlink: index, + }); + context.stack.push(index); + + let mut self_ref = false; + let mut lowlink = index; + for next in value.references.iter() { + let next_index = match traverse(next, context) { + Some(index) => index, + // There is nothing to do if the next variable has been + // fully resolved at this point. + None => { + continue; + }, + }; + let next_info = &context.var_info[next_index]; + if next_index > index { + // The next variable has a larger index than us, so it + // must be inserted in the recursive call above. We want + // to get its lowlink. + lowlink = cmp::min(lowlink, next_info.lowlink); + } else if next_index == index { + self_ref = true; + } else if next_info.name.is_some() { + // The next variable has a smaller order index and it is + // in the stack, so we are at the same component. + lowlink = cmp::min(lowlink, next_index); + } + } + + context.var_info[index].lowlink = lowlink; + if lowlink != index { + // This variable is in a loop, but it is not the root of + // this strong connected component. We simply return for + // now, and the root would remove it from the map. + // + // This cannot be removed from the map here, because + // otherwise the shortcut check at the beginning of this + // function would return the wrong value. + return Some(index); + } + + // This is the root of a strong-connected component. + let mut in_loop = self_ref; + let name; + loop { + let var_index = context + .stack + .pop() + .expect("The current variable should still be in stack"); + let var_info = &mut context.var_info[var_index]; + // We should never visit the variable again, so it's safe + // to take the name away, so that we don't do additional + // reference count. + let var_name = var_info + .name + .take() + .expect("Variable should not be poped from stack twice"); + if var_index == index { + name = var_name; + break; + } + // Anything here is in a loop which can traverse to the + // variable we are handling, so remove it from the map, it's invalid + // at computed-value time. + context.map.remove(&var_name); + in_loop = true; + } + if in_loop { + // This variable is in loop. Resolve to invalid. + context.map.remove(&name); + return None; + } + + // Now we have shown that this variable is not in a loop, and all of its + // dependencies should have been resolved. We can start substitution + // now. + let result = substitute_references_in_value(&value, &context.map, &context.device); + match result { + Ok(computed_value) => { + context.map.insert(name, computed_value); + }, + Err(..) => { + // This is invalid, reset it to the guaranteed-invalid value. + context.map.remove(&name); + }, + } + + // All resolved, so return the signal value. + None + } + + // Note that `seen` doesn't contain names inherited from our parent, but + // those can't have variable references (since we inherit the computed + // variables) so we don't want to spend cycles traversing them anyway. + for name in seen { + let mut context = Context { + count: 0, + index_map: PrecomputedHashMap::default(), + stack: SmallVec::new(), + var_info: SmallVec::new(), + map: custom_properties_map, + device, + }; + traverse(name, &mut context); + } +} + +/// Replace `var()` and `env()` functions in a pre-existing variable value. +fn substitute_references_in_value<'i>( + value: &'i VariableValue, + custom_properties: &CustomPropertiesMap, + device: &Device, +) -> Result<Arc<ComputedValue>, ParseError<'i>> { + debug_assert!(!value.references.is_empty() || value.references_environment); + + let mut input = ParserInput::new(&value.css); + let mut input = Parser::new(&mut input); + let mut position = (input.position(), value.first_token_type); + let mut computed_value = ComputedValue::empty(); + + let last_token_type = substitute_block( + &mut input, + &mut position, + &mut computed_value, + custom_properties, + device, + )?; + + computed_value.push_from(&input, position, last_token_type)?; + computed_value.css.shrink_to_fit(); + Ok(Arc::new(computed_value)) +} + +/// Replace `var()` functions in an arbitrary bit of input. +/// +/// If the variable has its initial value, the callback should return `Err(())` +/// and leave `partial_computed_value` unchanged. +/// +/// Otherwise, it should push the value of the variable (with its own `var()` functions replaced) +/// to `partial_computed_value` and return `Ok(last_token_type of what was pushed)` +/// +/// Return `Err(())` if `input` is invalid at computed-value time. +/// or `Ok(last_token_type that was pushed to partial_computed_value)` otherwise. +fn substitute_block<'i>( + input: &mut Parser<'i, '_>, + position: &mut (SourcePosition, TokenSerializationType), + partial_computed_value: &mut ComputedValue, + custom_properties: &CustomPropertiesMap, + device: &Device, +) -> Result<TokenSerializationType, ParseError<'i>> { + let mut last_token_type = TokenSerializationType::nothing(); + let mut set_position_at_next_iteration = false; + loop { + let before_this_token = input.position(); + let next = input.next_including_whitespace_and_comments(); + if set_position_at_next_iteration { + *position = ( + before_this_token, + match next { + Ok(token) => token.serialization_type(), + Err(_) => TokenSerializationType::nothing(), + }, + ); + set_position_at_next_iteration = false; + } + let token = match next { + Ok(token) => token, + Err(..) => break, + }; + match token { + Token::Function(ref name) + if name.eq_ignore_ascii_case("var") || name.eq_ignore_ascii_case("env") => + { + let is_env = name.eq_ignore_ascii_case("env"); + + partial_computed_value.push( + input, + input.slice(position.0..before_this_token), + position.1, + last_token_type, + )?; + input.parse_nested_block(|input| { + // parse_var_function() / parse_env_function() ensure neither .unwrap() will fail. + let name = { + let name = input.expect_ident().unwrap(); + if is_env { + Atom::from(&**name) + } else { + Atom::from(parse_name(&name).unwrap()) + } + }; + + let env_value; + let value = if is_env { + if let Some(v) = device.environment().get(&name, device) { + env_value = v; + Some(&env_value) + } else { + None + } + } else { + custom_properties.get(&name).map(|v| &**v) + }; + + if let Some(v) = value { + last_token_type = v.last_token_type; + partial_computed_value.push_variable(input, v)?; + // Skip over the fallback, as `parse_nested_block` would return `Err` + // if we don't consume all of `input`. + // FIXME: Add a specialized method to cssparser to do this with less work. + while input.next().is_ok() {} + } else { + input.expect_comma()?; + input.skip_whitespace(); + let after_comma = input.state(); + let first_token_type = input + .next_including_whitespace_and_comments() + .ok() + .map_or_else(TokenSerializationType::nothing, |t| { + t.serialization_type() + }); + input.reset(&after_comma); + let mut position = (after_comma.position(), first_token_type); + last_token_type = substitute_block( + input, + &mut position, + partial_computed_value, + custom_properties, + device, + )?; + partial_computed_value.push_from(input, position, last_token_type)?; + } + Ok(()) + })?; + set_position_at_next_iteration = true + }, + Token::Function(_) | + Token::ParenthesisBlock | + Token::CurlyBracketBlock | + Token::SquareBracketBlock => { + input.parse_nested_block(|input| { + substitute_block( + input, + position, + partial_computed_value, + custom_properties, + device, + ) + })?; + // It's the same type for CloseCurlyBracket and CloseSquareBracket. + last_token_type = Token::CloseParenthesis.serialization_type(); + }, + + _ => last_token_type = token.serialization_type(), + } + } + // FIXME: deal with things being implicitly closed at the end of the input. E.g. + // ```html + // <div style="--color: rgb(0,0,0"> + // <p style="background: var(--color) var(--image) top left; --image: url('a.png"></p> + // </div> + // ``` + Ok(last_token_type) +} + +/// Replace `var()` and `env()` functions for a non-custom property. +/// +/// Return `Err(())` for invalid at computed time. +pub fn substitute<'i>( + input: &'i str, + first_token_type: TokenSerializationType, + computed_values_map: Option<&Arc<CustomPropertiesMap>>, + device: &Device, +) -> Result<String, ParseError<'i>> { + let mut substituted = ComputedValue::empty(); + let mut input = ParserInput::new(input); + let mut input = Parser::new(&mut input); + let mut position = (input.position(), first_token_type); + let empty_map = CustomPropertiesMap::default(); + let custom_properties = match computed_values_map { + Some(m) => &**m, + None => &empty_map, + }; + let last_token_type = substitute_block( + &mut input, + &mut position, + &mut substituted, + &custom_properties, + device, + )?; + substituted.push_from(&input, position, last_token_type)?; + Ok(substituted.css) +} |