forked from mirrors/gecko-dev
In order to do it, expose the button padding to CSS via env(), and make the buttons just use the regular drawing. This slightly changes the padding to the end of the titlebar to match one half of the inter-button spacing, rather that however much padding the headerbar has. We could improve on this slightly by also exposing the headerbar padding and applying that to the last button, but that's not terribly easy to do due to us supporting re-ordering of the titlebar buttons, and reversing their placement, so it'd involve some rather hacky CSS. Not impossible, but not trivial, and this looks good enough IMO. Differential Revision: https://phabricator.services.mozilla.com/D202616
1964 lines
75 KiB
Rust
1964 lines
75 KiB
Rust
/* 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::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.
|
||
///
|
||
/// <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, 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 {
|
||
// 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"),
|
||
},
|
||
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()
|
||
}
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
|
||
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
|
||
// that, see https://github.com/w3c/csswg-drafts/issues/3262.
|
||
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("<EFBFBD>") && 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("<EFBFBD>")
|
||
}
|
||
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
|
||
// https://en.wikipedia.org/w/index.php?
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
if !defer {
|
||
substitute_references_if_needed_and_apply(
|
||
&name,
|
||
v,
|
||
&mut context.map,
|
||
context.stylist,
|
||
context.computed_context,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
context.non_custom_references = NonCustomReferences::default();
|
||
|
||
// 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(),
|
||
non_custom_index_map: NonCustomReferenceMap::default(),
|
||
stack: SmallVec::new(),
|
||
var_info: SmallVec::new(),
|
||
map: custom_properties_map,
|
||
non_custom_references: NonCustomReferences::default(),
|
||
stylist,
|
||
computed_context,
|
||
invalid_non_custom_properties,
|
||
deferred_properties: deferred_properties_map.as_deref_mut(),
|
||
};
|
||
traverse(
|
||
VarType::Custom((*name).clone()),
|
||
references_from_non_custom_properties,
|
||
&mut context,
|
||
);
|
||
}
|
||
}
|
||
|
||
// See https://drafts.csswg.org/css-variables-2/#invalid-at-computed-value-time
|
||
fn handle_invalid_at_computed_value_time(
|
||
name: &Name,
|
||
custom_properties: &mut ComputedCustomProperties,
|
||
inherited: &ComputedCustomProperties,
|
||
stylist: &Stylist,
|
||
is_root_element: bool,
|
||
) {
|
||
let registration = stylist.get_custom_property_registration(&name);
|
||
if !registration.syntax.is_universal() {
|
||
// For the root element, inherited maps are empty. We should just
|
||
// use the initial value if any, rather than removing the name.
|
||
if registration.inherits() && !is_root_element {
|
||
if let Some(value) = inherited.get(registration, name) {
|
||
custom_properties.insert(registration, name, Arc::clone(value));
|
||
return;
|
||
}
|
||
} else {
|
||
if let Some(ref initial_value) = registration.initial_value {
|
||
custom_properties.insert(registration, name, Arc::clone(initial_value));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
custom_properties.remove(registration, name);
|
||
}
|
||
|
||
/// Replace `var()` and `env()` functions in a pre-existing variable value.
|
||
fn substitute_references_if_needed_and_apply(
|
||
name: &Name,
|
||
value: &Arc<VariableValue>,
|
||
custom_properties: &mut ComputedCustomProperties,
|
||
stylist: &Stylist,
|
||
computed_context: &computed::Context,
|
||
) {
|
||
let registration = stylist.get_custom_property_registration(&name);
|
||
if !value.has_references() && registration.syntax.is_universal() {
|
||
// Trivial path: no references and no need to compute the value, just apply it directly.
|
||
custom_properties.insert(registration, name, Arc::clone(value));
|
||
return;
|
||
}
|
||
|
||
let inherited = computed_context.inherited_custom_properties();
|
||
let value = match substitute_internal(value, custom_properties, stylist, registration, computed_context) {
|
||
Ok(v) => v,
|
||
Err(..) => {
|
||
handle_invalid_at_computed_value_time(
|
||
name,
|
||
custom_properties,
|
||
inherited,
|
||
stylist,
|
||
computed_context.is_root_element(),
|
||
);
|
||
return;
|
||
},
|
||
}.into_value(&value.url_data);
|
||
|
||
// If variable fallback results in a wide keyword, deal with it now.
|
||
{
|
||
let mut input = ParserInput::new(&value.css);
|
||
let mut input = Parser::new(&mut input);
|
||
|
||
if let Ok(kw) = input.try_parse(CSSWideKeyword::parse) {
|
||
// TODO: It's unclear what this should do for revert / revert-layer, see
|
||
// https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset
|
||
// seems fine?
|
||
match (kw, registration.inherits(), computed_context.is_root_element()) {
|
||
(CSSWideKeyword::Initial, _, _) |
|
||
(CSSWideKeyword::Revert, false, _) |
|
||
(CSSWideKeyword::RevertLayer, false, _) |
|
||
(CSSWideKeyword::Unset, false, _) |
|
||
(CSSWideKeyword::Revert, true, true) |
|
||
(CSSWideKeyword::RevertLayer, true, true) |
|
||
(CSSWideKeyword::Unset, true, true) |
|
||
(CSSWideKeyword::Inherit, _, true) => {
|
||
custom_properties.remove(registration, name);
|
||
if let Some(ref initial_value) = registration.initial_value {
|
||
custom_properties.insert(registration, name, Arc::clone(initial_value));
|
||
}
|
||
},
|
||
(CSSWideKeyword::Revert, true, false) |
|
||
(CSSWideKeyword::RevertLayer, true, false) |
|
||
(CSSWideKeyword::Inherit, _, false) |
|
||
(CSSWideKeyword::Unset, true, false) => {
|
||
match inherited.get(registration, name) {
|
||
Some(value) => {
|
||
custom_properties.insert(registration, name, Arc::clone(value));
|
||
},
|
||
None => {
|
||
custom_properties.remove(registration, name);
|
||
},
|
||
};
|
||
},
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
custom_properties.insert(registration, name, Arc::new(value));
|
||
}
|
||
|
||
#[derive(Default)]
|
||
struct Substitution<'a> {
|
||
css: Cow<'a, str>,
|
||
first_token_type: TokenSerializationType,
|
||
last_token_type: TokenSerializationType,
|
||
}
|
||
|
||
impl<'a> Substitution<'a> {
|
||
fn new(
|
||
css: &'a str,
|
||
first_token_type: TokenSerializationType,
|
||
last_token_type: TokenSerializationType,
|
||
) -> Self {
|
||
Self {
|
||
css: Cow::Borrowed(css),
|
||
first_token_type,
|
||
last_token_type,
|
||
}
|
||
}
|
||
|
||
fn from_value(v: VariableValue) -> Substitution<'static> {
|
||
debug_assert!(!v.has_references(), "Computed values shouldn't have references");
|
||
Substitution {
|
||
css: Cow::from(v.css),
|
||
first_token_type: v.first_token_type,
|
||
last_token_type: v.last_token_type,
|
||
}
|
||
}
|
||
|
||
fn into_value(self, url_data: &UrlExtraData) -> VariableValue {
|
||
VariableValue {
|
||
css: self.css.into_owned(),
|
||
first_token_type: self.first_token_type,
|
||
last_token_type: self.last_token_type,
|
||
url_data: url_data.clone(),
|
||
references: Default::default(),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn compute_value(
|
||
css: &str,
|
||
url_data: &UrlExtraData,
|
||
registration: &PropertyRegistrationData,
|
||
computed_context: &computed::Context,
|
||
) -> Result<Substitution<'static>, ()> {
|
||
debug_assert!(!registration.syntax.is_universal());
|
||
|
||
let mut input = ParserInput::new(&css);
|
||
let mut input = Parser::new(&mut input);
|
||
|
||
let value = SpecifiedRegisteredValue::compute(
|
||
&mut input,
|
||
registration,
|
||
url_data,
|
||
computed_context,
|
||
AllowComputationallyDependent::Yes,
|
||
)?;
|
||
Ok(Substitution::from_value(value))
|
||
}
|
||
|
||
fn do_substitute_chunk<'a>(
|
||
css: &'a str,
|
||
start: usize,
|
||
end: usize,
|
||
first_token_type: TokenSerializationType,
|
||
last_token_type: TokenSerializationType,
|
||
url_data: &UrlExtraData,
|
||
custom_properties: &'a ComputedCustomProperties,
|
||
registration: &PropertyRegistrationData,
|
||
stylist: &Stylist,
|
||
computed_context: &computed::Context,
|
||
references: &mut std::iter::Peekable<std::slice::Iter<VarOrEnvReference>>,
|
||
) -> Result<Substitution<'a>, ()> {
|
||
if start == end {
|
||
// Empty string. Easy.
|
||
return Ok(Substitution::default());
|
||
}
|
||
// Easy case: no references involved.
|
||
if references
|
||
.peek()
|
||
.map_or(true, |reference| reference.end > end)
|
||
{
|
||
let result = &css[start..end];
|
||
if !registration.syntax.is_universal() {
|
||
return compute_value(result, url_data, registration, computed_context);
|
||
}
|
||
return Ok(Substitution::new(result, first_token_type, last_token_type));
|
||
}
|
||
|
||
let mut substituted = ComputedValue::empty(url_data);
|
||
let mut next_token_type = first_token_type;
|
||
let mut cur_pos = start;
|
||
while let Some(reference) = references.next_if(|reference| reference.end <= end) {
|
||
if reference.start != cur_pos {
|
||
substituted.push(
|
||
&css[cur_pos..reference.start],
|
||
next_token_type,
|
||
reference.prev_token_type,
|
||
)?;
|
||
}
|
||
|
||
let substitution = substitute_one_reference(
|
||
css,
|
||
url_data,
|
||
custom_properties,
|
||
reference,
|
||
stylist,
|
||
computed_context,
|
||
references,
|
||
)?;
|
||
|
||
// Optimize the property: var(--...) case to avoid allocating at all.
|
||
if reference.start == start && reference.end == end && registration.syntax.is_universal() {
|
||
return Ok(substitution);
|
||
}
|
||
|
||
substituted.push(
|
||
&substitution.css,
|
||
substitution.first_token_type,
|
||
substitution.last_token_type,
|
||
)?;
|
||
next_token_type = reference.next_token_type;
|
||
cur_pos = reference.end;
|
||
}
|
||
// Push the rest of the value if needed.
|
||
if cur_pos != end {
|
||
substituted.push(&css[cur_pos..end], next_token_type, last_token_type)?;
|
||
}
|
||
if !registration.syntax.is_universal() {
|
||
return compute_value(&substituted.css, url_data, registration, computed_context);
|
||
}
|
||
Ok(Substitution::from_value(substituted))
|
||
}
|
||
|
||
fn substitute_one_reference<'a>(
|
||
css: &'a str,
|
||
url_data: &UrlExtraData,
|
||
custom_properties: &'a ComputedCustomProperties,
|
||
reference: &VarOrEnvReference,
|
||
stylist: &Stylist,
|
||
computed_context: &computed::Context,
|
||
references: &mut std::iter::Peekable<std::slice::Iter<VarOrEnvReference>>,
|
||
) -> Result<Substitution<'a>, ()> {
|
||
let registration;
|
||
if reference.is_var {
|
||
registration = stylist.get_custom_property_registration(&reference.name);
|
||
if let Some(v) = custom_properties.get(registration, &reference.name) {
|
||
debug_assert!(!v.has_references(), "Should be already computed");
|
||
if registration.syntax.is_universal() {
|
||
// Skip references that are inside the outer variable (in fallback for example).
|
||
while references
|
||
.next_if(|next_ref| next_ref.end <= reference.end)
|
||
.is_some()
|
||
{}
|
||
} else {
|
||
// We need to validate the fallback if any, since invalid fallback should
|
||
// invalidate the whole variable.
|
||
if let Some(ref fallback) = reference.fallback {
|
||
let _ = do_substitute_chunk(
|
||
css,
|
||
fallback.start.get(),
|
||
reference.end - 1, // Don't include the closing parenthesis.
|
||
fallback.first_token_type,
|
||
fallback.last_token_type,
|
||
url_data,
|
||
custom_properties,
|
||
registration,
|
||
stylist,
|
||
computed_context,
|
||
references,
|
||
)?;
|
||
}
|
||
}
|
||
return Ok(Substitution {
|
||
css: Cow::from(&v.css),
|
||
first_token_type: v.first_token_type,
|
||
last_token_type: v.last_token_type,
|
||
});
|
||
}
|
||
} else {
|
||
registration = PropertyRegistrationData::unregistered();
|
||
let device = stylist.device();
|
||
if let Some(v) = device.environment().get(&reference.name, device, url_data) {
|
||
while references
|
||
.next_if(|next_ref| next_ref.end <= reference.end)
|
||
.is_some()
|
||
{}
|
||
return Ok(Substitution::from_value(v));
|
||
}
|
||
}
|
||
|
||
let Some(ref fallback) = reference.fallback else { return Err(()) };
|
||
|
||
do_substitute_chunk(
|
||
css,
|
||
fallback.start.get(),
|
||
reference.end - 1, // Skip the closing parenthesis of the reference value.
|
||
fallback.first_token_type,
|
||
fallback.last_token_type,
|
||
url_data,
|
||
custom_properties,
|
||
registration,
|
||
stylist,
|
||
computed_context,
|
||
references,
|
||
)
|
||
}
|
||
|
||
/// Replace `var()` and `env()` functions. Return `Err(..)` for invalid at computed time.
|
||
fn substitute_internal<'a>(
|
||
variable_value: &'a VariableValue,
|
||
custom_properties: &'a ComputedCustomProperties,
|
||
stylist: &Stylist,
|
||
registration: &PropertyRegistrationData,
|
||
computed_context: &computed::Context,
|
||
) -> Result<Substitution<'a>, ()> {
|
||
let mut refs = variable_value.references.refs.iter().peekable();
|
||
do_substitute_chunk(
|
||
&variable_value.css,
|
||
/* start = */ 0,
|
||
/* end = */ variable_value.css.len(),
|
||
variable_value.first_token_type,
|
||
variable_value.last_token_type,
|
||
&variable_value.url_data,
|
||
custom_properties,
|
||
registration,
|
||
stylist,
|
||
computed_context,
|
||
&mut refs,
|
||
)
|
||
}
|
||
|
||
/// Replace var() and env() functions, returning the resulting CSS string.
|
||
pub fn substitute<'a>(
|
||
variable_value: &'a VariableValue,
|
||
custom_properties: &'a ComputedCustomProperties,
|
||
stylist: &Stylist,
|
||
computed_context: &computed::Context,
|
||
) -> Result<Cow<'a, str>, ()> {
|
||
debug_assert!(variable_value.has_references());
|
||
let v = substitute_internal(
|
||
variable_value,
|
||
custom_properties,
|
||
stylist,
|
||
PropertyRegistrationData::unregistered(),
|
||
computed_context,
|
||
)?;
|
||
Ok(v.css)
|
||
}
|