Source code

Revision control

Copy as Markdown

Other Tools

/* 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].
//!
use crate::applicable_declarations::CascadePriority;
use crate::custom_properties_map::CustomPropertiesMap;
use crate::media_queries::Device;
use crate::properties::{
CSSWideKeyword, CustomDeclaration, CustomDeclarationValue, LonghandId, LonghandIdSet,
VariableDeclaration,
};
use crate::properties_and_values::{
registry::PropertyRegistrationData,
value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue},
};
use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet};
use crate::stylesheets::UrlExtraData;
use crate::stylist::Stylist;
use crate::values::computed;
use crate::values::specified::FontRelativeLength;
use crate::Atom;
use cssparser::{
CowRcStr, Delimiter, Parser, ParserInput, SourcePosition, Token, TokenSerializationType,
};
use selectors::parser::SelectorParseErrorKind;
use servo_arc::Arc;
use smallvec::SmallVec;
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::fmt::{self, Write};
use std::ops::{Index, IndexMut};
use std::{cmp, num};
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, url_data: &UrlExtraData) -> 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, url_data: &UrlExtraData) -> VariableValue {
VariableValue::pixels(device.safe_area_insets().top, url_data)
}
fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue {
VariableValue::pixels(device.safe_area_insets().bottom, url_data)
}
fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue {
VariableValue::pixels(device.safe_area_insets().left, url_data)
}
fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue {
VariableValue::pixels(device.safe_area_insets().right, url_data)
}
fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> 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",
},
url_data,
)
}
fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue {
VariableValue::pixels(device.scrollbar_inline_size().px(), url_data)
}
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, url_data: &UrlExtraData) -> VariableValue {
VariableValue::$ctor(lnf_int!($id), url_data)
}
make_variable!($atom, __eval)
}};
}
static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 8] = [
lnf_int_variable!(
atom!("-moz-gtk-csd-titlebar-button-spacing"),
TitlebarButtonSpacing,
int_pixels
),
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
),
lnf_int_variable!(
atom!("-moz-overlay-scrollbar-fade-duration"),
ScrollbarFadeDuration,
int_ms
),
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, url_data: &UrlExtraData) -> Option<VariableValue> {
if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) {
return Some((var.evaluator)(device, url_data));
}
if !url_data.chrome_rules_enabled() {
return None;
}
let var = CHROME_ENVIRONMENT_VARIABLES
.iter()
.find(|var| var.name == *name)?;
Some((var.evaluator)(device, url_data))
}
}
/// 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.
///
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, ToShmem)]
pub struct VariableValue {
/// The raw CSS string.
pub css: String,
/// The url data of the stylesheet where this value came from.
pub url_data: UrlExtraData,
first_token_type: TokenSerializationType,
last_token_type: TokenSerializationType,
/// var(), env(), or non-custom property (e.g. through `em`) references.
references: References,
}
trivial_to_computed_value!(VariableValue);
// For all purposes, we want values to be considered equal if their css text is equal.
impl PartialEq for VariableValue {
fn eq(&self, other: &Self) -> bool {
self.css == other.css
}
}
impl Eq for VariableValue {}
impl ToCss for SpecifiedValue {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: Write,
{
dest.write_str(&self.css)
}
}
/// A pair of separate CustomPropertiesMaps, split between custom properties
/// that have the inherit flag set and those with the flag unset.
#[repr(C)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ComputedCustomProperties {
/// Map for custom properties with inherit flag set, including non-registered
/// ones.
pub inherited: CustomPropertiesMap,
/// Map for custom properties with inherit flag unset.
pub non_inherited: CustomPropertiesMap,
}
impl ComputedCustomProperties {
/// Return whether the inherited and non_inherited maps are none.
pub fn is_empty(&self) -> bool {
self.inherited.is_empty() && self.non_inherited.is_empty()
}
/// Return the name and value of the property at specified index, if any.
pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> {
// Just expose the custom property items from custom_properties.inherited, followed
// by custom property items from custom_properties.non_inherited.
self.inherited
.get_index(index)
.or_else(|| self.non_inherited.get_index(index - self.inherited.len()))
}
/// Insert a custom property in the corresponding inherited/non_inherited
/// map, depending on whether the inherit flag is set or unset.
fn insert(
&mut self,
registration: &PropertyRegistrationData,
name: &Name,
value: Arc<VariableValue>,
) {
self.map_mut(registration).insert(name, value)
}
/// Remove a custom property from the corresponding inherited/non_inherited
/// map, depending on whether the inherit flag is set or unset.
fn remove(&mut self, registration: &PropertyRegistrationData, name: &Name) {
self.map_mut(registration).remove(name);
}
/// Shrink the capacity of the inherited maps as much as possible.
fn shrink_to_fit(&mut self) {
self.inherited.shrink_to_fit();
self.non_inherited.shrink_to_fit();
}
fn map_mut(&mut self, registration: &PropertyRegistrationData) -> &mut CustomPropertiesMap {
if registration.inherits() {
&mut self.inherited
} else {
&mut self.non_inherited
}
}
fn get(
&self,
registration: &PropertyRegistrationData,
name: &Name,
) -> Option<&Arc<VariableValue>> {
if registration.inherits() {
self.inherited.get(name)
} else {
self.non_inherited.get(name)
}
}
}
/// 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;
/// Set of flags to non-custom references this custom property makes.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)]
struct NonCustomReferences(u8);
bitflags! {
impl NonCustomReferences: u8 {
/// At least one custom property depends on font-relative units.
const FONT_UNITS = 1 << 0;
/// At least one custom property depends on root element's font-relative units.
const ROOT_FONT_UNITS = 1 << 1;
/// At least one custom property depends on line height units.
const LH_UNITS = 1 << 2;
/// At least one custom property depends on root element's line height units.
const ROOT_LH_UNITS = 1 << 3;
/// All dependencies not depending on the root element.
const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.bits() | Self::LH_UNITS.bits();
/// All dependencies depending on the root element.
const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.bits() | Self::ROOT_LH_UNITS.bits();
}
}
impl NonCustomReferences {
fn for_each<F>(&self, mut f: F)
where
F: FnMut(SingleNonCustomReference),
{
for (_, r) in self.iter_names() {
let single = match r {
Self::FONT_UNITS => SingleNonCustomReference::FontUnits,
Self::ROOT_FONT_UNITS => SingleNonCustomReference::RootFontUnits,
Self::LH_UNITS => SingleNonCustomReference::LhUnits,
Self::ROOT_LH_UNITS => SingleNonCustomReference::RootLhUnits,
_ => unreachable!("Unexpected single bit value"),
};
f(single);
}
}
fn from_unit(value: &CowRcStr) -> Self {
// For registered properties, any reference to font-relative dimensions
// make it dependent on font-related properties.
// TODO(dshin): When we unit algebra gets implemented and handled -
// Is it valid to say that `calc(1em / 2em * 3px)` triggers this?
if value.eq_ignore_ascii_case(FontRelativeLength::LH) {
return Self::FONT_UNITS | Self::LH_UNITS;
}
if value.eq_ignore_ascii_case(FontRelativeLength::EM) ||
value.eq_ignore_ascii_case(FontRelativeLength::EX) ||
value.eq_ignore_ascii_case(FontRelativeLength::CAP) ||
value.eq_ignore_ascii_case(FontRelativeLength::CH) ||
value.eq_ignore_ascii_case(FontRelativeLength::IC)
{
return Self::FONT_UNITS;
}
if value.eq_ignore_ascii_case(FontRelativeLength::RLH) {
return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS;
}
if value.eq_ignore_ascii_case(FontRelativeLength::REM) {
return Self::ROOT_FONT_UNITS;
}
Self::empty()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SingleNonCustomReference {
FontUnits = 0,
RootFontUnits,
LhUnits,
RootLhUnits,
}
struct NonCustomReferenceMap<T>([Option<T>; 4]);
impl<T> Default for NonCustomReferenceMap<T> {
fn default() -> Self {
NonCustomReferenceMap(Default::default())
}
}
impl<T> Index<SingleNonCustomReference> for NonCustomReferenceMap<T> {
type Output = Option<T>;
fn index(&self, reference: SingleNonCustomReference) -> &Self::Output {
&self.0[reference as usize]
}
}
impl<T> IndexMut<SingleNonCustomReference> for NonCustomReferenceMap<T> {
fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output {
&mut self.0[reference as usize]
}
}
/// Whether to defer resolving custom properties referencing font relative units.
#[derive(Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum DeferFontRelativeCustomPropertyResolution {
Yes,
No,
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
struct VariableFallback {
start: num::NonZeroUsize,
first_token_type: TokenSerializationType,
last_token_type: TokenSerializationType,
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
struct VarOrEnvReference {
name: Name,
start: usize,
end: usize,
fallback: Option<VariableFallback>,
prev_token_type: TokenSerializationType,
next_token_type: TokenSerializationType,
is_var: bool,
}
/// A struct holding information about the external references to that a custom
/// property value may have.
#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
struct References {
refs: Vec<VarOrEnvReference>,
non_custom_references: NonCustomReferences,
any_env: bool,
any_var: bool,
}
impl References {
fn has_references(&self) -> bool {
!self.refs.is_empty()
}
fn get_non_custom_dependencies(&self, is_root_element: bool) -> NonCustomReferences {
let mask = NonCustomReferences::NON_ROOT_DEPENDENCIES;
let mask = if is_root_element {
mask | NonCustomReferences::ROOT_DEPENDENCIES
} else {
mask
};
self.non_custom_references & mask
}
}
impl VariableValue {
fn empty(url_data: &UrlExtraData) -> Self {
Self {
css: String::new(),
last_token_type: Default::default(),
first_token_type: Default::default(),
url_data: url_data.clone(),
references: Default::default(),
}
}
/// Create a new custom property without parsing if the CSS is known to be valid and contain no
/// references.
pub fn new(
css: String,
url_data: &UrlExtraData,
first_token_type: TokenSerializationType,
last_token_type: TokenSerializationType,
) -> Self {
Self {
css,
url_data: url_data.clone(),
first_token_type,
last_token_type,
references: Default::default(),
}
}
fn push<'i>(
&mut self,
css: &str,
css_first_token_type: TokenSerializationType,
css_last_token_type: TokenSerializationType,
) -> Result<(), ()> {
/// 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(());
}
// 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(())
}
/// Parse a custom property value.
pub fn parse<'i, 't>(
input: &mut Parser<'i, 't>,
url_data: &UrlExtraData,
) -> Result<Self, ParseError<'i>> {
input.skip_whitespace();
let mut references = References::default();
let mut missing_closing_characters = String::new();
let start_position = input.position();
let (first_token_type, last_token_type) = parse_declaration_value(
input,
start_position,
&mut references,
&mut missing_closing_characters,
)?;
let mut css = input.slice_from(start_position).to_owned();
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.pop();
}
css.push_str(&missing_closing_characters);
}
css.shrink_to_fit();
references.refs.shrink_to_fit();
Ok(Self {
css,
url_data: url_data.clone(),
first_token_type,
last_token_type,
references,
})
}
/// Create VariableValue from an int.
fn integer(number: i32, url_data: &UrlExtraData) -> Self {
Self::from_token(
Token::Number {
has_sign: false,
value: number as f32,
int_value: Some(number),
},
url_data,
)
}
/// Create VariableValue from an int.
fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self {
Self::from_token(Token::Ident(ident.into()), url_data)
}
/// Create VariableValue from a float amount of CSS pixels.
fn pixels(number: f32, url_data: &UrlExtraData) -> Self {
// 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"),
},
url_data,
)
}
/// Create VariableValue from an integer amount of milliseconds.
fn int_ms(number: i32, url_data: &UrlExtraData) -> Self {
Self::from_token(
Token::Dimension {
has_sign: false,
value: number as f32,
int_value: Some(number),
unit: CowRcStr::from("ms"),
},
url_data,
)
}
/// Create VariableValue from an integer amount of CSS pixels.
fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self {
Self::from_token(
Token::Dimension {
has_sign: false,
value: number as f32,
int_value: Some(number),
unit: CowRcStr::from("px"),
},
url_data,
)
}
fn from_token(token: Token, url_data: &UrlExtraData) -> Self {
let token_type = token.serialization_type();
let mut css = token.to_css_string();
css.shrink_to_fit();
VariableValue {
css,
url_data: url_data.clone(),
first_token_type: token_type,
last_token_type: token_type,
references: Default::default(),
}
}
/// Returns the raw CSS text from this VariableValue
pub fn css_text(&self) -> &str {
&self.css
}
/// Returns whether this variable value has any reference to the environment or other
/// variables.
pub fn has_references(&self) -> bool {
self.references.has_references()
}
}
fn parse_declaration_value<'i, 't>(
input: &mut Parser<'i, 't>,
input_start: SourcePosition,
references: &mut References,
missing_closing_characters: &mut String,
) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> {
input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
parse_declaration_value_block(input, input_start, 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>,
input_start: SourcePosition,
references: &mut References,
missing_closing_characters: &mut String,
) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> {
let mut is_first = true;
let mut first_token_type = TokenSerializationType::Nothing;
let mut last_token_type = TokenSerializationType::Nothing;
let mut prev_reference_index: Option<usize> = None;
loop {
let token_start = input.position();
let Ok(token) = input.next_including_whitespace_and_comments() else { break };
let prev_token_type = last_token_type;
let serialization_type = token.serialization_type();
last_token_type = serialization_type;
if is_first {
first_token_type = last_token_type;
is_first = false;
}
macro_rules! nested {
() => {
input.parse_nested_block(|input| {
parse_declaration_value_block(
input,
input_start,
references,
missing_closing_characters,
)
})?
};
}
macro_rules! check_closed {
($closing:expr) => {
if !input.slice_from(token_start).ends_with($closing) {
missing_closing_characters.push_str($closing)
}
};
}
if let Some(index) = prev_reference_index.take() {
references.refs[index].next_token_type = serialization_type;
}
match *token {
Token::Comment(_) => {
let token_slice = input.slice_from(token_start);
if !token_slice.ends_with("*/") {
missing_closing_characters.push_str(if token_slice.ends_with('*') {
"/"
} else {
"*/"
})
}
},
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) => {
let is_var = name.eq_ignore_ascii_case("var");
if is_var || name.eq_ignore_ascii_case("env") {
let our_ref_index = references.refs.len();
let fallback = input.parse_nested_block(|input| {
// TODO(emilio): For env() this should be <custom-ident> per spec, but no other browser does
let name = input.expect_ident()?;
let name = Atom::from(if is_var {
match parse_name(name.as_ref()) {
Ok(name) => name,
Err(()) => {
let name = name.clone();
return Err(input.new_custom_error(
SelectorParseErrorKind::UnexpectedIdent(name),
));
},
}
} else {
name.as_ref()
});
// We want the order of the references to match source order. So we need to reserve our slot
// now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since
// if this fails we discard the whole result anyways.
let start = token_start.byte_index() - input_start.byte_index();
references.refs.push(VarOrEnvReference {
name,
start,
// To be fixed up after parsing fallback and auto-closing via our_ref_index.
end: start,
prev_token_type,
// To be fixed up (if needed) on the next loop iteration via prev_reference_index.
next_token_type: TokenSerializationType::Nothing,
// To be fixed up after parsing fallback.
fallback: None,
is_var,
});
let mut fallback = None;
if input.try_parse(|input| input.expect_comma()).is_ok() {
input.skip_whitespace();
let fallback_start = num::NonZeroUsize::new(
input.position().byte_index() - input_start.byte_index(),
)
.unwrap();
// NOTE(emilio): Intentionally using parse_declaration_value rather than
// parse_declaration_value_block, since that's what parse_fallback used to do.
let (first, last) = parse_declaration_value(
input,
input_start,
references,
missing_closing_characters,
)?;
fallback = Some(VariableFallback {
start: fallback_start,
first_token_type: first,
last_token_type: last,
});
} else {
let state = input.state();
// We still need to consume the rest of the potentially-unclosed
// tokens, but make sure to not consume tokens that would otherwise be
// invalid, by calling reset().
parse_declaration_value_block(
input,
input_start,
references,
missing_closing_characters,
)?;
input.reset(&state);
}
Ok(fallback)
})?;
check_closed!(")");
prev_reference_index = Some(our_ref_index);
let reference = &mut references.refs[our_ref_index];
reference.end = input.position().byte_index() - input_start.byte_index() + missing_closing_characters.len();
reference.fallback = fallback;
if is_var {
references.any_var = true;
} else {
references.any_env = true;
}
} else {
nested!();
check_closed!(")");
}
},
Token::ParenthesisBlock => {
nested!();
check_closed!(")");
},
Token::CurlyBracketBlock => {
nested!();
check_closed!("}");
},
Token::SquareBracketBlock => {
nested!();
check_closed!("]");
},
Token::QuotedString(_) => {
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)
}
},
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, ..
} => {
references
.non_custom_references
.insert(NonCustomReferences::from_unit(value));
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!(")");
}
},
_ => {},
};
}
Ok((first_token_type, last_token_type))
}
/// A struct that takes care of encapsulating the cascade process for custom properties.
pub struct CustomPropertiesBuilder<'a, 'b: 'a> {
seen: PrecomputedHashSet<&'a Name>,
may_have_cycles: bool,
custom_properties: ComputedCustomProperties,
reverted: PrecomputedHashMap<&'a Name, (CascadePriority, bool)>,
stylist: &'a Stylist,
computed_context: &'a mut computed::Context<'b>,
references_from_non_custom_properties: NonCustomReferenceMap<Vec<Name>>,
}
impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> {
/// Create a new builder, inheriting from a given custom properties map.
///
/// We expose this publicly mostly for @keyframe blocks.
pub fn new_with_properties(stylist: &'a Stylist, custom_properties: ComputedCustomProperties, computed_context: &'a mut computed::Context<'b>) -> Self {
Self {
seen: PrecomputedHashSet::default(),
reverted: Default::default(),
may_have_cycles: false,
custom_properties,
stylist,
computed_context,
references_from_non_custom_properties: NonCustomReferenceMap::default(),
}
}
/// Create a new builder, inheriting from the right style given context.
pub fn new(stylist: &'a Stylist, context: &'a mut computed::Context<'b>) -> Self {
let is_root_element = context.is_root_element();
let inherited = context.inherited_custom_properties();
let initial_values = stylist.get_custom_property_initial_values();
let properties = ComputedCustomProperties {
inherited: if is_root_element {
debug_assert!(inherited.is_empty());
initial_values.inherited.clone()
} else {
inherited.inherited.clone()
},
non_inherited: initial_values.non_inherited.clone(),
};
// Reuse flags from computing registered custom properties initial values, such as
// whether they depend on viewport units.
context.style().add_flags(stylist.get_custom_property_initial_values_flags());
Self::new_with_properties(stylist, properties, context)
}
/// 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;
}
let map = &mut self.custom_properties;
let registration = self.stylist.get_custom_property_registration(&name);
match *value {
CustomDeclarationValue::Value(ref unparsed_value) => {
let has_custom_property_references = unparsed_value.references.any_var;
let registered_length_property =
registration.syntax.may_reference_font_relative_length();
// Non-custom dependency is really relevant for registered custom properties
// that require computed value of such dependencies.
let has_non_custom_dependencies = registered_length_property &&
!unparsed_value
.references
.get_non_custom_dependencies(self.computed_context.is_root_element())
.is_empty();
self.may_have_cycles |=
has_custom_property_references || has_non_custom_dependencies;
// If the variable value has no references to other properties, perform
// substitution here instead of forcing a full traversal in `substitute_all`
// afterwards.
if !has_custom_property_references && !has_non_custom_dependencies {
return substitute_references_if_needed_and_apply(
name,
unparsed_value,
map,
self.stylist,
self.computed_context,
);
}
map.insert(registration, name, Arc::clone(unparsed_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 => {
// For non-inherited custom properties, 'initial' was handled in value_may_affect_style.
debug_assert!(registration.inherits(), "Should've been handled earlier");
map.remove(registration, name);
if let Some(ref initial_value) = registration.initial_value {
map.insert(registration, name, initial_value.clone());
}
},
CSSWideKeyword::Inherit => {
// For inherited custom properties, 'inherit' was handled in value_may_affect_style.
debug_assert!(!registration.inherits(), "Should've been handled earlier");
if let Some(inherited_value) = self
.computed_context
.inherited_custom_properties()
.non_inherited
.get(name)
{
map.insert(registration, name, inherited_value.clone());
}
},
// handled in value_may_affect_style
CSSWideKeyword::Unset => unreachable!(),
},
}
}
/// Note a non-custom property with variable reference that may in turn depend on that property.
/// e.g. `font-size` depending on a custom property that may be a registered property using `em`.
pub fn note_potentially_cyclic_non_custom_dependency(&mut self, id: LonghandId, decl: &VariableDeclaration) {
// With unit algebra in `calc()`, references aren't limited to `font-size`.
// For example, `--foo: 100ex; font-weight: calc(var(--foo) / 1ex);`,
// or `--foo: 1em; zoom: calc(var(--foo) * 30px / 2em);`
let references = match id {
LonghandId::FontSize => {
if self.computed_context.is_root_element() {
NonCustomReferences::ROOT_FONT_UNITS
} else {
NonCustomReferences::FONT_UNITS
}
},
LonghandId::LineHeight => {
if self.computed_context.is_root_element() {
NonCustomReferences::ROOT_LH_UNITS |
NonCustomReferences::ROOT_FONT_UNITS
} else {
NonCustomReferences::LH_UNITS | NonCustomReferences::FONT_UNITS
}
},
_ => return,
};
let refs = &decl.value.variable_value.references;
if !refs.any_var {
return;
}
let variables: Vec<Atom> = refs.refs.iter().filter_map(|reference| {
if !reference.is_var {
return None;
}
if !self.stylist.get_custom_property_registration(&reference.name).syntax.may_compute_length() {
return None;
}
Some(reference.name.clone())
}).collect();
references.for_each(|idx| {
let entry = &mut self.references_from_non_custom_properties[idx];
let was_none = entry.is_none();
let v = entry.get_or_insert_with(|| variables.clone());
if was_none {
return;
}
v.extend(variables.clone().into_iter());
});
}
fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool {
let registration = self.stylist.get_custom_property_registration(&name);
match *value {
CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => {
// For inherited custom properties, explicit 'inherit' means we
// can just use any existing value in the inherited
// CustomPropertiesMap.
if registration.inherits() {
return false;
}
},
CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial) => {
// For non-inherited custom properties, explicit 'initial' means
// we can just use any initial value in the registration.
if !registration.inherits() {
return false;
}
},
CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) => {
// Explicit 'unset' means we can either just use any existing
// value in the inherited CustomPropertiesMap or the initial
// value in the registration.
return false;
},
_ => {},
}
let existing_value = self.custom_properties.get(registration, &name);
match (existing_value, value) {
(None, &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial)) => {
debug_assert!(registration.inherits(), "Should've been handled earlier");
// The initial value of a custom property without a
// guaranteed-invalid initial value is the same as it
// not existing in the map.
if registration.initial_value.is_none() {
return false;
}
},
(
Some(existing_value),
&CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial),
) => {
debug_assert!(registration.inherits(), "Should've been handled earlier");
// Don't bother overwriting an existing value with the initial value specified in
// the registration.
if Some(existing_value) == registration.initial_value.as_ref() {
return false;
}
},
(Some(_), &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit)) => {
debug_assert!(!registration.inherits(), "Should've been handled earlier");
// existing_value is the registered initial value.
// Don't bother adding it to self.custom_properties.non_inherited
// if the key is also absent from self.inherited.non_inherited.
if self
.computed_context
.inherited_custom_properties()
.non_inherited
.get(name)
.is_none()
{
return false;
}
},
(Some(existing_value), &CustomDeclarationValue::Value(ref value)) => {
// Don't bother overwriting an existing value with the same
// specified value.
if existing_value == value {
return false;
}
},
_ => {},
}
true
}
/// Computes the map of applicable custom properties, as well as
/// longhand properties that are now considered invalid-at-compute time.
/// The result is saved into the computed context.
///
/// If there was any specified property or non-inherited custom property
/// with an initial value, we've created a new map and now we
/// need to remove any potential cycles (And marking non-custom
/// properties), and wrap it in an arc.
///
/// Some registered custom properties may require font-related properties
/// be resolved to resolve. If these properties are not resolved at this time,
/// `defer` should be set to `Yes`, which will leave such custom properties,
/// and other properties referencing them, untouched. These properties are
/// returned separately, to be resolved by `build_deferred` to fully resolve
/// all custom properties after all necessary non-custom properties are resolved.
pub fn build(
mut self,
defer: DeferFontRelativeCustomPropertyResolution,
) -> Option<ComputedCustomProperties> {
let mut deferred_custom_properties = None;
if self.may_have_cycles {
if defer == DeferFontRelativeCustomPropertyResolution::Yes {
deferred_custom_properties = Some(ComputedCustomProperties::default());
}
let mut invalid_non_custom_properties = LonghandIdSet::default();
substitute_all(
&mut self.custom_properties,
deferred_custom_properties.as_mut(),
&mut invalid_non_custom_properties,
&self.seen,
&self.references_from_non_custom_properties,
self.stylist,
self.computed_context,
);
self.computed_context.builder.invalid_non_custom_properties = invalid_non_custom_properties;
}
self.custom_properties.shrink_to_fit();
// 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.
let initial_values = self.stylist.get_custom_property_initial_values();
self.computed_context.builder.custom_properties = ComputedCustomProperties {
inherited: if self
.computed_context
.inherited_custom_properties()
.inherited == self.custom_properties.inherited
{
self.computed_context
.inherited_custom_properties()
.inherited
.clone()
} else {
self.custom_properties.inherited
},
non_inherited: if initial_values.non_inherited == self.custom_properties.non_inherited {
initial_values.non_inherited.clone()
} else {
self.custom_properties.non_inherited
},
};
deferred_custom_properties
}
/// Fully resolve all deferred custom properties, assuming that the incoming context
/// has necessary properties resolved.
pub fn build_deferred(
deferred: ComputedCustomProperties,
stylist: &Stylist,
computed_context: &mut computed::Context,
) {
if deferred.is_empty() {
return;
}
// Guaranteed to not have cycles at this point.
let substitute =
|deferred: &CustomPropertiesMap,
stylist: &Stylist,
context: &computed::Context,
custom_properties: &mut ComputedCustomProperties| {
// Since `CustomPropertiesMap` preserves insertion order, we shouldn't
// have to worry about resolving in a wrong order.
for (k, v) in deferred.iter() {
let Some(v) = v else { continue };
substitute_references_if_needed_and_apply(
k,
v,
custom_properties,
stylist,
context,
);
}
};
let mut custom_properties = std::mem::take(&mut computed_context.builder.custom_properties);
substitute(
&deferred.inherited,
stylist,
computed_context,
&mut custom_properties,
);
substitute(
&deferred.non_inherited,
stylist,
computed_context,
&mut custom_properties,
);
computed_context.builder.custom_properties = custom_properties;
}
}
/// 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 ComputedCustomProperties,
mut deferred_properties_map: Option<&mut ComputedCustomProperties>,
invalid_non_custom_properties: &mut LonghandIdSet,
seen: &PrecomputedHashSet<&Name>,
references_from_non_custom_properties: &NonCustomReferenceMap<Vec<Name>>,
stylist: &Stylist,
computed_context: &computed::Context,
) {
// The cycle dependencies removal in this function is a variant
// of Tarjan's algorithm. It is mostly based on the pseudo-code
// listed in
// title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495
#[derive(Clone, Eq, PartialEq, Debug)]
enum VarType {
Custom(Name),
NonCustom(SingleNonCustomReference),
}
/// 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.
var: Option<VarType>,
/// 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.
struct Context<'a, 'b: '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>,
/// Mapping from a non-custom dependency to its order index.
non_custom_index_map: NonCustomReferenceMap<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]>,
/// References to non-custom properties in this strongly connected component.
non_custom_references: NonCustomReferences,
map: &'a mut ComputedCustomProperties,
/// The stylist is used to get registered properties, and to resolve the environment to
/// substitute `env()` variables.
stylist: &'a Stylist,
/// The computed context is used to get inherited custom
/// properties and compute registered custom properties.
computed_context: &'a computed::Context<'b>,
/// Longhand IDs that became invalid due to dependency cycle(s).
invalid_non_custom_properties: &'a mut LonghandIdSet,
/// Properties that cannot yet be substituted.
deferred_properties: Option<&'a mut ComputedCustomProperties>,
}
/// 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, 'b>(
var: VarType,
non_custom_references: &NonCustomReferenceMap<Vec<Name>>,
context: &mut Context<'a, 'b>,
) -> Option<usize> {
// Some shortcut checks.
let (value, should_substitute) = match var {
VarType::Custom(ref name) => {
let registration = context.stylist.get_custom_property_registration(name);
let value = context.map.get(registration, name)?;
let non_custom_references = value
.references
.get_non_custom_dependencies(context.computed_context.is_root_element());
let has_custom_property_reference = value.references.any_var;
// Nothing to resolve.
if !has_custom_property_reference && non_custom_references.is_empty() {
debug_assert!(!value.references.any_env, "Should've been handled earlier");
return None;
}
// Has this variable been visited?
match context.index_map.entry(name.clone()) {
Entry::Occupied(entry) => {
return Some(*entry.get());
},
Entry::Vacant(entry) => {
entry.insert(context.count);
},
}
context.non_custom_references |= value.as_ref().references.non_custom_references;
// Hold a strong reference to the value so that we don't
// need to keep reference to context.map.
(Some(value.clone()), has_custom_property_reference)
},
VarType::NonCustom(ref non_custom) => {
let entry = &mut context.non_custom_index_map[*non_custom];
if let Some(v) = entry {
return Some(*v);
}
*entry = Some(context.count);
(None, false)
},
};
// 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 {
var: Some(var.clone()),
lowlink: index,
});
context.stack.push(index);
let mut self_ref = false;
let mut lowlink = index;
let visit_link =
|var: VarType, context: &mut Context, lowlink: &mut usize, self_ref: &mut bool| {
let next_index = match traverse(var, non_custom_references, context) {
Some(index) => index,
// There is nothing to do if the next variable has been
// fully resolved at this point.
None => {
return;
},
};
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.var.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);
}
};
if let Some(ref v) = value.as_ref() {
debug_assert!(
matches!(var, VarType::Custom(_)),
"Non-custom property has references?"
);
// Visit other custom properties...
// FIXME: Maybe avoid visiting the same var twice if not needed?
for next in &v.references.refs {
if !next.is_var {
continue;
}
visit_link(
VarType::Custom(next.name.clone()),
context,
&mut lowlink,
&mut self_ref,
);
}
// ... Then non-custom properties.
v.references.non_custom_references.for_each(|r| {
visit_link(VarType::NonCustom(r), context, &mut lowlink, &mut self_ref);
});
} else if let VarType::NonCustom(non_custom) = var {
let entry = &non_custom_references[non_custom];
if let Some(deps) = entry.as_ref() {
for d in deps {
// Visit any reference from this non-custom property to custom properties.
visit_link(
VarType::Custom(d.clone()),
context,
&mut lowlink,
&mut self_ref,
);
}
}
}
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;
let handle_variable_in_loop = |name: &Name, context: &mut Context<'a, 'b>| {
if context
.non_custom_references
.intersects(NonCustomReferences::FONT_UNITS | NonCustomReferences::ROOT_FONT_UNITS)
{
context
.invalid_non_custom_properties
.insert(LonghandId::FontSize);
}
if context.non_custom_references.intersects(
NonCustomReferences::LH_UNITS |
NonCustomReferences::ROOT_LH_UNITS,
) {
context
.invalid_non_custom_properties
.insert(LonghandId::LineHeight);
}
// This variable is in loop. Resolve to invalid.
handle_invalid_at_computed_value_time(
name,
context.map,
context.computed_context.inherited_custom_properties(),
context.stylist,
context.computed_context.is_root_element(),
);
};
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
.var
.take()
.expect("Variable should not be poped from stack twice");
if var_index == index {
name = match var_name {
VarType::Custom(name) => name,
// At the root of this component, and it's a non-custom
// reference - we have nothing to substitute, so
// it's effectively resolved.
VarType::NonCustom(..) => return None,
};
break;
}
if let VarType::Custom(name) = var_name {
// Anything here is in a loop which can traverse to the
// variable we are handling, so it's invalid at
// computed-value time.
handle_variable_in_loop(&name, context);
}
in_loop = true;
}
// We've gotten to the root of this strongly connected component, so clear
// whether or not it involved non-custom references.
// It's fine to track it like this, because non-custom properties currently
// being tracked can only participate in any loop only once.
if in_loop {
handle_variable_in_loop(&name, context);
context.non_custom_references = NonCustomReferences::default();
return None;
}
if let Some(ref v) = value.as_ref() {
let registration = context.stylist.get_custom_property_registration(&name);
let registered_length_property =
registration.syntax.may_reference_font_relative_length();
let mut defer = false;
if !context.non_custom_references.is_empty() && registered_length_property {
if let Some(deferred) = &mut context.deferred_properties {
// This property directly depends on a non-custom property, defer resolving it.
deferred.insert(registration, &name, (*v).clone());
context.map.remove(registration, &name);
defer = true;
}
}
if should_substitute && !defer {
for reference in v.references.refs.iter() {
if !reference.is_var {
continue;
}
if let Some(deferred) = &mut context.deferred_properties {
let registration =
context.stylist.get_custom_property_registration(&reference.name);
if deferred.get(registration, &reference.name).is_some() {
// This property depends on a custom property that depends on a non-custom property, defer.
deferred.insert(registration, &name, Arc::clone(v));
context.map.remove(registration, &name);
defer = true;
break;
}
}
}
<