blob: 83a2927306afdba4179a9042857efe9f2058c566 [file] [edit]
//! CSS custom properties and unparsed token values.
use crate::error::{ParserError, PrinterError, PrinterErrorKind};
use crate::macros::enum_property;
use crate::prefixes::Feature;
use crate::printer::Printer;
use crate::properties::PropertyId;
use crate::rules::supports::SupportsCondition;
use crate::stylesheet::ParserOptions;
use crate::targets::{should_compile, Features, Targets};
use crate::traits::{Parse, ParseWithOptions, ToCss};
use crate::values::angle::Angle;
use crate::values::color::{
parse_hsl_hwb_components, parse_rgb_components, ColorFallbackKind, ComponentParser, CssColor, LightDarkColor,
HSL, RGB, RGBA,
};
use crate::values::ident::{CustomIdent, DashedIdent, DashedIdentReference, Ident};
use crate::values::length::{serialize_dimension, LengthValue};
use crate::values::number::CSSInteger;
use crate::values::percentage::Percentage;
use crate::values::resolution::Resolution;
use crate::values::string::CowArcStr;
use crate::values::time::Time;
use crate::values::url::Url;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::color::parse_hash_color;
use cssparser::*;
use super::AnimationName;
#[cfg(feature = "serde")]
use crate::serialization::ValueWrapper;
/// A CSS custom property, representing any unknown property.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct CustomProperty<'i> {
/// The name of the property.
#[cfg_attr(feature = "serde", serde(borrow))]
pub name: CustomPropertyName<'i>,
/// The property value, stored as a raw token list.
pub value: TokenList<'i>,
}
impl<'i> CustomProperty<'i> {
/// Parses a custom property with the given name.
pub fn parse<'t>(
name: CustomPropertyName<'i>,
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
TokenList::parse(input, options, 0)
})?;
Ok(CustomProperty { name, value })
}
}
/// A CSS custom property name.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum CustomPropertyName<'i> {
/// An author-defined CSS custom property.
#[cfg_attr(feature = "serde", serde(borrow))]
Custom(DashedIdent<'i>),
/// An unknown CSS property.
Unknown(Ident<'i>),
}
impl<'i> From<CowArcStr<'i>> for CustomPropertyName<'i> {
fn from(name: CowArcStr<'i>) -> Self {
if name.starts_with("--") {
CustomPropertyName::Custom(DashedIdent(name))
} else {
CustomPropertyName::Unknown(Ident(name))
}
}
}
impl<'i> From<CowRcStr<'i>> for CustomPropertyName<'i> {
fn from(name: CowRcStr<'i>) -> Self {
CustomPropertyName::from(CowArcStr::from(name))
}
}
impl<'i> AsRef<str> for CustomPropertyName<'i> {
#[inline]
fn as_ref(&self) -> &str {
match self {
CustomPropertyName::Custom(c) => c.as_ref(),
CustomPropertyName::Unknown(u) => u.as_ref(),
}
}
}
impl<'i> ToCss for CustomPropertyName<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
CustomPropertyName::Custom(c) => c.to_css(dest),
CustomPropertyName::Unknown(u) => u.to_css(dest),
}
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'i, 'de: 'i> serde::Deserialize<'de> for CustomPropertyName<'i> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let name = CowArcStr::deserialize(deserializer)?;
Ok(name.into())
}
}
/// A known property with an unparsed value.
///
/// This type is used when the value of a known property could not
/// be parsed, e.g. in the case css `var()` references are encountered.
/// In this case, the raw tokens are stored instead.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct UnparsedProperty<'i> {
/// The id of the property.
pub property_id: PropertyId<'i>,
/// The property value, stored as a raw token list.
#[cfg_attr(feature = "serde", serde(borrow))]
pub value: TokenList<'i>,
}
impl<'i> UnparsedProperty<'i> {
/// Parses a property with the given id as a token list.
pub fn parse<'t>(
property_id: PropertyId<'i>,
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
TokenList::parse(input, options, 0)
})?;
Ok(UnparsedProperty { property_id, value })
}
pub(crate) fn get_prefixed(&self, targets: Targets, feature: Feature) -> UnparsedProperty<'i> {
let mut clone = self.clone();
let prefix = self.property_id.prefix();
clone.property_id = clone.property_id.with_prefix(targets.prefixes(prefix.or_none(), feature));
clone
}
/// Returns a new UnparsedProperty with the same value and the given property id.
pub fn with_property_id(&self, property_id: PropertyId<'i>) -> UnparsedProperty<'i> {
UnparsedProperty {
property_id,
value: self.value.clone(),
}
}
/// Substitutes variables and re-parses the property.
#[cfg(feature = "substitute_variables")]
#[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
pub fn substitute_variables<'x>(
mut self,
vars: &std::collections::HashMap<&str, TokenList<'i>>,
) -> Result<super::Property<'x>, ()> {
use super::Property;
use crate::stylesheet::PrinterOptions;
use static_self::IntoOwned;
// Substitute variables in the token list.
self.value.substitute_variables(vars);
// Now stringify and re-parse the property to its fully parsed form.
// Ideally we'd be able to reuse the tokens rather than printing, but cssparser doesn't provide a way to do that.
let mut css = String::new();
let mut dest = Printer::new(&mut css, PrinterOptions::default());
self.value.to_css(&mut dest, false).unwrap();
let property =
Property::parse_string(self.property_id.clone(), &css, ParserOptions::default()).map_err(|_| ())?;
Ok(property.into_owned())
}
}
/// A raw list of CSS tokens, with embedded parsed values.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token_list, TOKENS))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct TokenList<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub Vec<TokenOrValue<'i>>);
/// A raw CSS token, or a parsed value.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token, TOKENS), visit_types(TOKENS | COLORS | URLS | VARIABLES | ENVIRONMENT_VARIABLES | FUNCTIONS | LENGTHS | ANGLES | TIMES | RESOLUTIONS | DASHED_IDENTS))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum TokenOrValue<'i> {
/// A token.
#[cfg_attr(feature = "serde", serde(borrow))]
Token(Token<'i>),
/// A parsed CSS color.
Color(CssColor),
/// A color with unresolved components.
UnresolvedColor(UnresolvedColor<'i>),
/// A parsed CSS url.
Url(Url<'i>),
/// A CSS variable reference.
Var(Variable<'i>),
/// A CSS environment variable reference.
Env(EnvironmentVariable<'i>),
/// A custom CSS function.
Function(Function<'i>),
/// A length.
Length(LengthValue),
/// An angle.
Angle(Angle),
/// A time.
Time(Time),
/// A resolution.
Resolution(Resolution),
/// A dashed ident.
DashedIdent(DashedIdent<'i>),
/// An animation name.
AnimationName(AnimationName<'i>),
}
impl<'i> From<Token<'i>> for TokenOrValue<'i> {
fn from(token: Token<'i>) -> TokenOrValue<'i> {
TokenOrValue::Token(token)
}
}
impl<'i> TokenOrValue<'i> {
/// Returns whether the token is whitespace.
pub fn is_whitespace(&self) -> bool {
matches!(self, TokenOrValue::Token(Token::WhiteSpace(_)))
}
}
impl<'a> Eq for TokenOrValue<'a> {}
impl<'a> std::hash::Hash for TokenOrValue<'a> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let tag = std::mem::discriminant(self);
tag.hash(state);
match self {
TokenOrValue::Token(t) => t.hash(state),
_ => {
// This function is primarily used to deduplicate selectors.
// Values inside selectors should be exceedingly rare and implementing
// Hash for them is somewhat complex due to floating point values.
// For now, we just ignore them, which only means there are more
// hash collisions. For such a rare case this is probably fine.
}
}
}
}
impl<'i> ParseWithOptions<'i> for TokenList<'i> {
fn parse_with_options<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
TokenList::parse(input, options, 0)
}
}
impl<'i> TokenList<'i> {
pub(crate) fn parse<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
depth: usize,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut tokens = vec![];
TokenList::parse_into(input, &mut tokens, options, depth)?;
// Slice off leading and trailing whitespace if there are at least two tokens.
// If there is only one token, we must preserve it. e.g. `--foo: ;` is valid.
if tokens.len() >= 2 {
let mut slice = &tokens[..];
if matches!(tokens.first(), Some(token) if token.is_whitespace()) {
slice = &slice[1..];
}
if matches!(tokens.last(), Some(token) if token.is_whitespace()) {
slice = &slice[..slice.len() - 1];
}
return Ok(TokenList(slice.to_vec()));
}
return Ok(TokenList(tokens));
}
pub(crate) fn parse_raw<'t>(
input: &mut Parser<'i, 't>,
tokens: &mut Vec<TokenOrValue<'i>>,
options: &ParserOptions<'_, 'i>,
depth: usize,
) -> Result<(), ParseError<'i, ParserError<'i>>> {
if depth > 500 {
return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
}
loop {
let state = input.state();
match input.next_including_whitespace_and_comments() {
Ok(token @ &cssparser::Token::ParenthesisBlock)
| Ok(token @ &cssparser::Token::SquareBracketBlock)
| Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
tokens.push(Token::from(token).into());
let closing_delimiter = match token {
cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
_ => unreachable!(),
};
input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
tokens.push(closing_delimiter.into());
}
Ok(token @ &cssparser::Token::Function(_)) => {
tokens.push(Token::from(token).into());
input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
tokens.push(Token::CloseParenthesis.into());
}
Ok(token) if token.is_parse_error() => {
return Err(ParseError {
kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
location: state.source_location(),
})
}
Ok(token) => {
tokens.push(Token::from(token).into());
}
Err(_) => break,
}
}
Ok(())
}
fn parse_into<'t>(
input: &mut Parser<'i, 't>,
tokens: &mut Vec<TokenOrValue<'i>>,
options: &ParserOptions<'_, 'i>,
depth: usize,
) -> Result<(), ParseError<'i, ParserError<'i>>> {
if depth > 500 {
return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
}
let mut last_is_delim = false;
let mut last_is_whitespace = false;
loop {
let state = input.state();
match input.next_including_whitespace_and_comments() {
Ok(&cssparser::Token::WhiteSpace(..)) | Ok(&cssparser::Token::Comment(..)) => {
// Skip whitespace if the last token was a delimiter.
// Otherwise, replace all whitespace and comments with a single space character.
if !last_is_delim {
tokens.push(Token::WhiteSpace(" ".into()).into());
last_is_whitespace = true;
}
}
Ok(&cssparser::Token::Function(ref f)) => {
// Attempt to parse embedded color values into hex tokens.
let f = f.into();
if let Some(color) = try_parse_color_token(&f, &state, input) {
tokens.push(TokenOrValue::Color(color));
last_is_delim = false;
last_is_whitespace = false;
} else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) {
tokens.push(TokenOrValue::UnresolvedColor(color));
last_is_delim = true;
last_is_whitespace = false;
} else if f == "url" {
input.reset(&state);
tokens.push(TokenOrValue::Url(Url::parse(input)?));
last_is_delim = false;
last_is_whitespace = false;
} else if f == "var" {
let var = input.parse_nested_block(|input| {
let var = Variable::parse(input, options, depth + 1)?;
Ok(TokenOrValue::Var(var))
})?;
tokens.push(var);
last_is_delim = true;
last_is_whitespace = false;
} else if f == "env" {
let env = input.parse_nested_block(|input| {
let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?;
Ok(TokenOrValue::Env(env))
})?;
tokens.push(env);
last_is_delim = true;
last_is_whitespace = false;
} else {
let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?;
tokens.push(TokenOrValue::Function(Function {
name: Ident(f),
arguments,
}));
last_is_delim = true; // Whitespace is not required after any of these chars.
last_is_whitespace = false;
}
}
Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => {
if let Ok((r, g, b, a)) = parse_hash_color(h.as_bytes()) {
tokens.push(TokenOrValue::Color(CssColor::RGBA(RGBA::new(r, g, b, a))));
} else {
tokens.push(Token::Hash(h.into()).into());
}
last_is_delim = false;
last_is_whitespace = false;
}
Ok(&cssparser::Token::UnquotedUrl(_)) => {
input.reset(&state);
tokens.push(TokenOrValue::Url(Url::parse(input)?));
last_is_delim = false;
last_is_whitespace = false;
}
Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => {
tokens.push(TokenOrValue::DashedIdent(name.into()));
last_is_delim = false;
last_is_whitespace = false;
}
Ok(token @ &cssparser::Token::ParenthesisBlock)
| Ok(token @ &cssparser::Token::SquareBracketBlock)
| Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
tokens.push(Token::from(token).into());
let closing_delimiter = match token {
cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
_ => unreachable!(),
};
input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?;
tokens.push(closing_delimiter.into());
last_is_delim = true; // Whitespace is not required after any of these chars.
last_is_whitespace = false;
}
Ok(token @ cssparser::Token::Dimension { .. }) => {
let value = if let Ok(length) = LengthValue::try_from(token) {
TokenOrValue::Length(length)
} else if let Ok(angle) = Angle::try_from(token) {
TokenOrValue::Angle(angle)
} else if let Ok(time) = Time::try_from(token) {
TokenOrValue::Time(time)
} else if let Ok(resolution) = Resolution::try_from(token) {
TokenOrValue::Resolution(resolution)
} else {
TokenOrValue::Token(token.into())
};
tokens.push(value);
last_is_delim = false;
last_is_whitespace = false;
}
Ok(token) if token.is_parse_error() => {
return Err(ParseError {
kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
location: state.source_location(),
})
}
Ok(token) => {
last_is_delim = matches!(token, cssparser::Token::Delim(_) | cssparser::Token::Comma);
// If this is a delimiter, and the last token was whitespace,
// replace the whitespace with the delimiter since both are not required.
if last_is_delim && last_is_whitespace {
let last = tokens.last_mut().unwrap();
*last = Token::from(token).into();
} else {
tokens.push(Token::from(token).into());
}
last_is_whitespace = false;
}
Err(_) => break,
}
}
Ok(())
}
}
#[inline]
fn try_parse_color_token<'i, 't>(
f: &CowArcStr<'i>,
state: &ParserState,
input: &mut Parser<'i, 't>,
) -> Option<CssColor> {
match_ignore_ascii_case! { &*f,
"rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" => {
let s = input.state();
input.reset(&state);
if let Ok(color) = CssColor::parse(input) {
return Some(color)
}
input.reset(&s);
},
_ => {}
}
None
}
impl<'i> TokenList<'i> {
pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if !dest.minify && self.0.len() == 1 && matches!(self.0.first(), Some(token) if token.is_whitespace()) {
return Ok(());
}
let mut has_whitespace = false;
for (i, token_or_value) in self.0.iter().enumerate() {
has_whitespace = match token_or_value {
TokenOrValue::Color(color) => {
color.to_css(dest)?;
false
}
TokenOrValue::UnresolvedColor(color) => {
color.to_css(dest, is_custom_property)?;
false
}
TokenOrValue::Url(url) => {
if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() {
return Err(dest.error(
PrinterErrorKind::AmbiguousUrlInCustomProperty {
url: url.url.as_ref().to_owned(),
},
url.loc,
));
}
url.to_css(dest)?;
false
}
TokenOrValue::Var(var) => {
var.to_css(dest, is_custom_property)?;
self.write_whitespace_if_needed(i, dest)?
}
TokenOrValue::Env(env) => {
env.to_css(dest, is_custom_property)?;
self.write_whitespace_if_needed(i, dest)?
}
TokenOrValue::Function(f) => {
f.to_css(dest, is_custom_property)?;
self.write_whitespace_if_needed(i, dest)?
}
TokenOrValue::Length(v) => {
// Do not serialize unitless zero lengths in custom properties as it may break calc().
let (value, unit) = v.to_unit_value();
serialize_dimension(value, unit, dest)?;
false
}
TokenOrValue::Angle(v) => {
v.to_css(dest)?;
false
}
TokenOrValue::Time(v) => {
v.to_css(dest)?;
false
}
TokenOrValue::Resolution(v) => {
v.to_css(dest)?;
false
}
TokenOrValue::DashedIdent(v) => {
v.to_css(dest)?;
false
}
TokenOrValue::AnimationName(v) => {
v.to_css(dest)?;
false
}
TokenOrValue::Token(token) => match token {
Token::Delim(d) => {
if *d == '+' || *d == '-' {
dest.write_char(' ')?;
dest.write_char(*d)?;
dest.write_char(' ')?;
} else {
let ws_before = !has_whitespace && (*d == '/' || *d == '*');
dest.delim(*d, ws_before)?;
}
true
}
Token::Comma => {
dest.delim(',', false)?;
true
}
Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => {
token.to_css(dest)?;
self.write_whitespace_if_needed(i, dest)?
}
Token::Dimension { value, unit, .. } => {
serialize_dimension(*value, unit, dest)?;
false
}
Token::Number { value, .. } => {
value.to_css(dest)?;
false
}
_ => {
token.to_css(dest)?;
matches!(token, Token::WhiteSpace(..))
}
},
};
}
Ok(())
}
pub(crate) fn to_css_raw<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
for token_or_value in &self.0 {
match token_or_value {
TokenOrValue::Token(token) => {
token.to_css(dest)?;
}
_ => {
return Err(PrinterError {
kind: PrinterErrorKind::FmtError,
loc: None,
})
}
}
}
Ok(())
}
#[inline]
fn write_whitespace_if_needed<W>(&self, i: usize, dest: &mut Printer<W>) -> Result<bool, PrinterError>
where
W: std::fmt::Write,
{
if !dest.minify
&& i != self.0.len() - 1
&& !matches!(
self.0[i + 1],
TokenOrValue::Token(Token::Comma) | TokenOrValue::Token(Token::CloseParenthesis)
)
{
// Whitespace is removed during parsing, so add it back if we aren't minifying.
dest.write_char(' ')?;
Ok(true)
} else {
Ok(false)
}
}
}
/// A raw CSS token.
// Copied from cssparser to change CowRcStr to CowArcStr
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Token<'a> {
/// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram)
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
Ident(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'a>),
/// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
///
/// The value does not include the `@` marker.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
AtKeyword(CowArcStr<'a>),
/// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted"
///
/// The value does not include the `#` marker.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
Hash(CowArcStr<'a>),
/// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id"
///
/// The value does not include the `#` marker.
#[cfg_attr(feature = "serde", serde(rename = "id-hash", with = "ValueWrapper::<CowArcStr>"))]
IDHash(CowArcStr<'a>), // Hash that is a valid ID selector.
/// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram)
///
/// The value does not include the quotes.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
String(CowArcStr<'a>),
/// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram)
///
/// The value does not include the `url(` `)` markers. Note that `url( <string-token> )` is represented by a
/// `Function` token.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
UnquotedUrl(CowArcStr<'a>),
/// A `<delim-token>`
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<char>"))]
Delim(char),
/// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram)
Number {
/// Whether the number had a `+` or `-` sign.
///
/// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
#[cfg_attr(feature = "serde", serde(skip))]
has_sign: bool,
/// The value as a float
value: f32,
/// If the origin source did not include a fractional part, the value as an integer.
#[cfg_attr(feature = "serde", serde(skip))]
int_value: Option<i32>,
},
/// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
Percentage {
/// Whether the number had a `+` or `-` sign.
#[cfg_attr(feature = "serde", serde(skip))]
has_sign: bool,
/// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
#[cfg_attr(feature = "serde", serde(rename = "value"))]
unit_value: f32,
/// If the origin source did not include a fractional part, the value as an integer.
/// It is **not** divided by 100.
#[cfg_attr(feature = "serde", serde(skip))]
int_value: Option<i32>,
},
/// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
Dimension {
/// Whether the number had a `+` or `-` sign.
///
/// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
#[cfg_attr(feature = "serde", serde(skip))]
has_sign: bool,
/// The value as a float
value: f32,
/// If the origin source did not include a fractional part, the value as an integer.
#[cfg_attr(feature = "serde", serde(skip))]
int_value: Option<i32>,
/// The unit, e.g. "px" in `12px`
unit: CowArcStr<'a>,
},
/// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
WhiteSpace(CowArcStr<'a>),
/// A comment.
///
/// The CSS Syntax spec does not generate tokens for comments,
/// But we do, because we can (borrowed &str makes it cheap).
///
/// The value does not include the `/*` `*/` markers.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
Comment(CowArcStr<'a>),
/// A `:` `<colon-token>`
Colon, // :
/// A `;` `<semicolon-token>`
Semicolon, // ;
/// A `,` `<comma-token>`
Comma, // ,
/// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram)
IncludeMatch,
/// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram)
DashMatch,
/// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram)
PrefixMatch,
/// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram)
SuffixMatch,
/// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
SubstringMatch,
/// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram)
#[cfg_attr(feature = "serde", serde(rename = "cdo"))]
CDO,
/// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram)
#[cfg_attr(feature = "serde", serde(rename = "cdc"))]
CDC,
/// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram)
///
/// The value (name) does not include the `(` marker.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
Function(CowArcStr<'a>),
/// A `<(-token>`
ParenthesisBlock,
/// A `<[-token>`
SquareBracketBlock,
/// A `<{-token>`
CurlyBracketBlock,
/// A `<bad-url-token>`
///
/// This token always indicates a parse error.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
BadUrl(CowArcStr<'a>),
/// A `<bad-string-token>`
///
/// This token always indicates a parse error.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
BadString(CowArcStr<'a>),
/// A `<)-token>`
///
/// When obtained from one of the `Parser::next*` methods,
/// this token is always unmatched and indicates a parse error.
CloseParenthesis,
/// A `<]-token>`
///
/// When obtained from one of the `Parser::next*` methods,
/// this token is always unmatched and indicates a parse error.
CloseSquareBracket,
/// A `<}-token>`
///
/// When obtained from one of the `Parser::next*` methods,
/// this token is always unmatched and indicates a parse error.
CloseCurlyBracket,
}
impl<'a> From<&cssparser::Token<'a>> for Token<'a> {
#[inline]
fn from(t: &cssparser::Token<'a>) -> Token<'a> {
match t {
cssparser::Token::Ident(x) => Token::Ident(x.into()),
cssparser::Token::AtKeyword(x) => Token::AtKeyword(x.into()),
cssparser::Token::Hash(x) => Token::Hash(x.into()),
cssparser::Token::IDHash(x) => Token::IDHash(x.into()),
cssparser::Token::QuotedString(x) => Token::String(x.into()),
cssparser::Token::UnquotedUrl(x) => Token::UnquotedUrl(x.into()),
cssparser::Token::Function(x) => Token::Function(x.into()),
cssparser::Token::BadUrl(x) => Token::BadUrl(x.into()),
cssparser::Token::BadString(x) => Token::BadString(x.into()),
cssparser::Token::Delim(c) => Token::Delim(*c),
cssparser::Token::Number {
has_sign,
value,
int_value,
} => Token::Number {
has_sign: *has_sign,
value: *value,
int_value: *int_value,
},
cssparser::Token::Dimension {
has_sign,
value,
int_value,
unit,
} => Token::Dimension {
has_sign: *has_sign,
value: *value,
int_value: *int_value,
unit: unit.into(),
},
cssparser::Token::Percentage {
has_sign,
unit_value,
int_value,
} => Token::Percentage {
has_sign: *has_sign,
unit_value: *unit_value,
int_value: *int_value,
},
cssparser::Token::WhiteSpace(w) => Token::WhiteSpace((*w).into()),
cssparser::Token::Comment(c) => Token::Comment((*c).into()),
cssparser::Token::Colon => Token::Colon,
cssparser::Token::Semicolon => Token::Semicolon,
cssparser::Token::Comma => Token::Comma,
cssparser::Token::IncludeMatch => Token::IncludeMatch,
cssparser::Token::DashMatch => Token::DashMatch,
cssparser::Token::PrefixMatch => Token::PrefixMatch,
cssparser::Token::SuffixMatch => Token::SuffixMatch,
cssparser::Token::SubstringMatch => Token::SubstringMatch,
cssparser::Token::CDO => Token::CDO,
cssparser::Token::CDC => Token::CDC,
cssparser::Token::ParenthesisBlock => Token::ParenthesisBlock,
cssparser::Token::SquareBracketBlock => Token::SquareBracketBlock,
cssparser::Token::CurlyBracketBlock => Token::CurlyBracketBlock,
cssparser::Token::CloseParenthesis => Token::CloseParenthesis,
cssparser::Token::CloseSquareBracket => Token::CloseSquareBracket,
cssparser::Token::CloseCurlyBracket => Token::CloseCurlyBracket,
}
}
}
impl<'a> ToCss for Token<'a> {
#[inline]
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use cssparser::ToCss;
match self {
Token::Ident(x) => cssparser::Token::Ident(x.as_ref().into()).to_css(dest)?,
Token::AtKeyword(x) => cssparser::Token::AtKeyword(x.as_ref().into()).to_css(dest)?,
Token::Hash(x) => cssparser::Token::Hash(x.as_ref().into()).to_css(dest)?,
Token::IDHash(x) => cssparser::Token::IDHash(x.as_ref().into()).to_css(dest)?,
Token::String(x) => cssparser::Token::QuotedString(x.as_ref().into()).to_css(dest)?,
Token::UnquotedUrl(x) => cssparser::Token::UnquotedUrl(x.as_ref().into()).to_css(dest)?,
Token::Function(x) => cssparser::Token::Function(x.as_ref().into()).to_css(dest)?,
Token::BadUrl(x) => cssparser::Token::BadUrl(x.as_ref().into()).to_css(dest)?,
Token::BadString(x) => cssparser::Token::BadString(x.as_ref().into()).to_css(dest)?,
Token::Delim(c) => cssparser::Token::Delim(*c).to_css(dest)?,
Token::Number {
has_sign,
value,
int_value,
} => cssparser::Token::Number {
has_sign: *has_sign,
value: *value,
int_value: *int_value,
}
.to_css(dest)?,
Token::Dimension {
has_sign,
value,
int_value,
unit,
} => cssparser::Token::Dimension {
has_sign: *has_sign,
value: *value,
int_value: *int_value,
unit: unit.as_ref().into(),
}
.to_css(dest)?,
Token::Percentage {
has_sign,
unit_value,
int_value,
} => cssparser::Token::Percentage {
has_sign: *has_sign,
unit_value: *unit_value,
int_value: *int_value,
}
.to_css(dest)?,
Token::WhiteSpace(w) => cssparser::Token::WhiteSpace(w).to_css(dest)?,
Token::Comment(c) => cssparser::Token::Comment(c).to_css(dest)?,
Token::Colon => cssparser::Token::Colon.to_css(dest)?,
Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?,
Token::Comma => cssparser::Token::Comma.to_css(dest)?,
Token::IncludeMatch => cssparser::Token::IncludeMatch.to_css(dest)?,
Token::DashMatch => cssparser::Token::DashMatch.to_css(dest)?,
Token::PrefixMatch => cssparser::Token::PrefixMatch.to_css(dest)?,
Token::SuffixMatch => cssparser::Token::SuffixMatch.to_css(dest)?,
Token::SubstringMatch => cssparser::Token::SubstringMatch.to_css(dest)?,
Token::CDO => cssparser::Token::CDO.to_css(dest)?,
Token::CDC => cssparser::Token::CDC.to_css(dest)?,
Token::ParenthesisBlock => cssparser::Token::ParenthesisBlock.to_css(dest)?,
Token::SquareBracketBlock => cssparser::Token::SquareBracketBlock.to_css(dest)?,
Token::CurlyBracketBlock => cssparser::Token::CurlyBracketBlock.to_css(dest)?,
Token::CloseParenthesis => cssparser::Token::CloseParenthesis.to_css(dest)?,
Token::CloseSquareBracket => cssparser::Token::CloseSquareBracket.to_css(dest)?,
Token::CloseCurlyBracket => cssparser::Token::CloseCurlyBracket.to_css(dest)?,
}
Ok(())
}
}
impl<'a> Eq for Token<'a> {}
impl<'a> std::hash::Hash for Token<'a> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let tag = std::mem::discriminant(self);
tag.hash(state);
match self {
Token::Ident(x) => x.hash(state),
Token::AtKeyword(x) => x.hash(state),
Token::Hash(x) => x.hash(state),
Token::IDHash(x) => x.hash(state),
Token::String(x) => x.hash(state),
Token::UnquotedUrl(x) => x.hash(state),
Token::Function(x) => x.hash(state),
Token::BadUrl(x) => x.hash(state),
Token::BadString(x) => x.hash(state),
Token::Delim(x) => x.hash(state),
Token::Number {
has_sign,
value,
int_value,
} => {
has_sign.hash(state);
integer_decode(*value).hash(state);
int_value.hash(state);
}
Token::Dimension {
has_sign,
value,
int_value,
unit,
} => {
has_sign.hash(state);
integer_decode(*value).hash(state);
int_value.hash(state);
unit.hash(state);
}
Token::Percentage {
has_sign,
unit_value,
int_value,
} => {
has_sign.hash(state);
integer_decode(*unit_value).hash(state);
int_value.hash(state);
}
Token::WhiteSpace(w) => w.hash(state),
Token::Comment(c) => c.hash(state),
Token::Colon
| Token::Semicolon
| Token::Comma
| Token::IncludeMatch
| Token::DashMatch
| Token::PrefixMatch
| Token::SuffixMatch
| Token::SubstringMatch
| Token::CDO
| Token::CDC
| Token::ParenthesisBlock
| Token::SquareBracketBlock
| Token::CurlyBracketBlock
| Token::CloseParenthesis
| Token::CloseSquareBracket
| Token::CloseCurlyBracket => {}
}
}
}
/// Converts a floating point value into its mantissa, exponent,
/// and sign components so that it can be hashed.
fn integer_decode(v: f32) -> (u32, i16, i8) {
let bits: u32 = unsafe { std::mem::transmute(v) };
let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 };
let mut exponent: i16 = ((bits >> 23) & 0xff) as i16;
let mantissa = if exponent == 0 {
(bits & 0x7fffff) << 1
} else {
(bits & 0x7fffff) | 0x800000
};
// Exponent bias + mantissa shift
exponent -= 127 + 23;
(mantissa, exponent, sign)
}
impl<'i> TokenList<'i> {
pub(crate) fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
let mut fallbacks = ColorFallbackKind::empty();
for token in &self.0 {
match token {
TokenOrValue::Color(color) => {
fallbacks |= color.get_possible_fallbacks(targets);
}
TokenOrValue::Function(f) => {
fallbacks |= f.arguments.get_necessary_fallbacks(targets);
}
TokenOrValue::Var(v) => {
if let Some(fallback) = &v.fallback {
fallbacks |= fallback.get_necessary_fallbacks(targets);
}
}
TokenOrValue::Env(v) => {
if let Some(fallback) = &v.fallback {
fallbacks |= fallback.get_necessary_fallbacks(targets);
}
}
_ => {}
}
}
fallbacks
}
pub(crate) fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
let tokens = self
.0
.iter()
.map(|token| match token {
TokenOrValue::Color(color) => TokenOrValue::Color(color.get_fallback(kind)),
TokenOrValue::Function(f) => TokenOrValue::Function(f.get_fallback(kind)),
TokenOrValue::Var(v) => TokenOrValue::Var(v.get_fallback(kind)),
TokenOrValue::Env(e) => TokenOrValue::Env(e.get_fallback(kind)),
_ => token.clone(),
})
.collect();
TokenList(tokens)
}
pub(crate) fn get_fallbacks(&mut self, targets: Targets) -> Vec<(SupportsCondition<'i>, Self)> {
// Get the full list of possible fallbacks, and remove the lowest one, which will replace
// the original declaration. The remaining fallbacks need to be added as @supports rules.
let mut fallbacks = self.get_necessary_fallbacks(targets);
let lowest_fallback = fallbacks.lowest();
fallbacks.remove(lowest_fallback);
let mut res = Vec::new();
if fallbacks.contains(ColorFallbackKind::P3) {
res.push((
ColorFallbackKind::P3.supports_condition(),
self.get_fallback(ColorFallbackKind::P3),
));
}
if fallbacks.contains(ColorFallbackKind::LAB) {
res.push((
ColorFallbackKind::LAB.supports_condition(),
self.get_fallback(ColorFallbackKind::LAB),
));
}
if !lowest_fallback.is_empty() {
for token in self.0.iter_mut() {
match token {
TokenOrValue::Color(color) => {
*color = color.get_fallback(lowest_fallback);
}
TokenOrValue::Function(f) => *f = f.get_fallback(lowest_fallback),
TokenOrValue::Var(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
TokenOrValue::Env(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
_ => {}
}
}
}
res
}
pub(crate) fn get_features(&self) -> Features {
let mut features = Features::empty();
for token in &self.0 {
match token {
TokenOrValue::Color(color) => {
features |= color.get_features();
}
TokenOrValue::UnresolvedColor(unresolved_color) => {
features |= Features::SpaceSeparatedColorNotation;
match unresolved_color {
UnresolvedColor::LightDark { light, dark } => {
features |= Features::LightDark;
features |= light.get_features();
features |= dark.get_features();
}
_ => {}
}
}
TokenOrValue::Function(f) => {
features |= f.arguments.get_features();
}
TokenOrValue::Var(v) => {
if let Some(fallback) = &v.fallback {
features |= fallback.get_features();
}
}
TokenOrValue::Env(v) => {
if let Some(fallback) = &v.fallback {
features |= fallback.get_features();
}
}
_ => {}
}
}
features
}
/// Substitutes variables with the provided values.
#[cfg(feature = "substitute_variables")]
#[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
pub fn substitute_variables(&mut self, vars: &std::collections::HashMap<&str, TokenList<'i>>) {
self.visit(&mut VarInliner { vars }).unwrap()
}
}
#[cfg(feature = "substitute_variables")]
struct VarInliner<'a, 'i> {
vars: &'a std::collections::HashMap<&'a str, TokenList<'i>>,
}
#[cfg(feature = "substitute_variables")]
impl<'a, 'i> crate::visitor::Visitor<'i> for VarInliner<'a, 'i> {
type Error = std::convert::Infallible;
fn visit_types(&self) -> crate::visitor::VisitTypes {
crate::visit_types!(TOKENS | VARIABLES)
}
fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) -> Result<(), Self::Error> {
let mut i = 0;
let mut seen = std::collections::HashSet::new();
while i < tokens.0.len() {
let token = &mut tokens.0[i];
token.visit(self).unwrap();
if let TokenOrValue::Var(var) = token {
if let Some(value) = self.vars.get(var.name.ident.0.as_ref()) {
// Ignore circular references.
if seen.insert(var.name.ident.0.clone()) {
tokens.0.splice(i..i + 1, value.0.iter().cloned());
// Don't advance. We need to replace any variables in the value.
continue;
}
} else if let Some(fallback) = &var.fallback {
let fallback = fallback.0.clone();
if seen.insert(var.name.ident.0.clone()) {
tokens.0.splice(i..i + 1, fallback.into_iter());
continue;
}
}
}
seen.clear();
i += 1;
}
Ok(())
}
}
/// A CSS variable reference.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_variable, VARIABLES))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Variable<'i> {
/// The variable name.
#[cfg_attr(feature = "serde", serde(borrow))]
pub name: DashedIdentReference<'i>,
/// A fallback value in case the variable is not defined.
pub fallback: Option<TokenList<'i>>,
}
impl<'i> Variable<'i> {
fn parse<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
depth: usize,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let name = DashedIdentReference::parse_with_options(input, options)?;
let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
Some(TokenList::parse(input, options, depth)?)
} else {
None
};
Ok(Variable { name, fallback })
}
fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
dest.write_str("var(")?;
self.name.to_css(dest)?;
if let Some(fallback) = &self.fallback {
dest.delim(',', false)?;
fallback.to_css(dest, is_custom_property)?;
}
dest.write_char(')')
}
fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
Variable {
name: self.name.clone(),
fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
}
}
}
/// A CSS environment variable reference.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "visitor",
derive(Visit),
visit(visit_environment_variable, ENVIRONMENT_VARIABLES)
)]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct EnvironmentVariable<'i> {
/// The environment variable name.
#[cfg_attr(feature = "serde", serde(borrow))]
pub name: EnvironmentVariableName<'i>,
/// Optional indices into the dimensions of the environment variable.
#[cfg_attr(feature = "serde", serde(default))]
pub indices: Vec<CSSInteger>,
/// A fallback value in case the variable is not defined.
pub fallback: Option<TokenList<'i>>,
}
/// A CSS environment variable name.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "lowercase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum EnvironmentVariableName<'i> {
/// A UA-defined environment variable.
#[cfg_attr(
feature = "serde",
serde(with = "crate::serialization::ValueWrapper::<UAEnvironmentVariable>")
)]
UA(UAEnvironmentVariable),
/// A custom author-defined environment variable.
#[cfg_attr(feature = "serde", serde(borrow))]
Custom(DashedIdentReference<'i>),
/// An unknown environment variable.
#[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CustomIdent>"))]
Unknown(CustomIdent<'i>),
}
enum_property! {
/// A UA-defined environment variable name.
pub enum UAEnvironmentVariable {
/// The safe area inset from the top of the viewport.
SafeAreaInsetTop,
/// The safe area inset from the right of the viewport.
SafeAreaInsetRight,
/// The safe area inset from the bottom of the viewport.
SafeAreaInsetBottom,
/// The safe area inset from the left of the viewport.
SafeAreaInsetLeft,
/// The viewport segment width.
ViewportSegmentWidth,
/// The viewport segment height.
ViewportSegmentHeight,
/// The viewport segment top position.
ViewportSegmentTop,
/// The viewport segment left position.
ViewportSegmentLeft,
/// The viewport segment bottom position.
ViewportSegmentBottom,
/// The viewport segment right position.
ViewportSegmentRight,
}
}
impl<'i> EnvironmentVariableName<'i> {
/// Returns the name of the environment variable as a string.
pub fn name(&self) -> &str {
match self {
EnvironmentVariableName::UA(ua) => ua.as_str(),
EnvironmentVariableName::Custom(c) => c.ident.as_ref(),
EnvironmentVariableName::Unknown(u) => u.0.as_ref(),
}
}
}
impl<'i> Parse<'i> for EnvironmentVariableName<'i> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if let Ok(ua) = input.try_parse(UAEnvironmentVariable::parse) {
return Ok(EnvironmentVariableName::UA(ua));
}
if let Ok(dashed) =
input.try_parse(|input| DashedIdentReference::parse_with_options(input, &ParserOptions::default()))
{
return Ok(EnvironmentVariableName::Custom(dashed));
}
let ident = CustomIdent::parse(input)?;
return Ok(EnvironmentVariableName::Unknown(ident));
}
}
impl<'i> ToCss for EnvironmentVariableName<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
EnvironmentVariableName::UA(ua) => ua.to_css(dest),
EnvironmentVariableName::Custom(custom) => custom.to_css(dest),
EnvironmentVariableName::Unknown(unknown) => unknown.to_css(dest),
}
}
}
impl<'i> EnvironmentVariable<'i> {
pub(crate) fn parse<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
depth: usize,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
input.expect_function_matching("env")?;
input.parse_nested_block(|input| Self::parse_nested(input, options, depth))
}
pub(crate) fn parse_nested<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
depth: usize,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let name = EnvironmentVariableName::parse(input)?;
let mut indices = Vec::new();
while let Ok(index) = input.try_parse(CSSInteger::parse) {
indices.push(index);
}
let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
Some(TokenList::parse(input, options, depth + 1)?)
} else {
None
};
Ok(EnvironmentVariable {
name,
indices,
fallback,
})
}
pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
dest.write_str("env(")?;
self.name.to_css(dest)?;
for item in &self.indices {
dest.write_char(' ')?;
item.to_css(dest)?;
}
if let Some(fallback) = &self.fallback {
dest.delim(',', false)?;
fallback.to_css(dest, is_custom_property)?;
}
dest.write_char(')')
}
fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
EnvironmentVariable {
name: self.name.clone(),
indices: self.indices.clone(),
fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
}
}
}
/// A custom CSS function.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(feature = "visitor", visit(visit_function, FUNCTIONS))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Function<'i> {
/// The function name.
#[cfg_attr(feature = "serde", serde(borrow))]
pub name: Ident<'i>,
/// The function arguments.
pub arguments: TokenList<'i>,
}
impl<'i> Function<'i> {
fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
self.name.to_css(dest)?;
dest.write_char('(')?;
self.arguments.to_css(dest, is_custom_property)?;
dest.write_char(')')
}
fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
Function {
name: self.name.clone(),
arguments: self.arguments.get_fallback(kind),
}
}
}
/// A color value with an unresolved alpha value (e.g. a variable).
/// These can be converted from the modern slash syntax to older comma syntax.
/// This can only be done when the only unresolved component is the alpha
/// since variables can resolve to multiple tokens.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "lowercase")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum UnresolvedColor<'i> {
/// An rgb() color.
RGB {
/// The red component.
r: f32,
/// The green component.
g: f32,
/// The blue component.
b: f32,
/// The unresolved alpha component.
#[cfg_attr(feature = "serde", serde(borrow))]
alpha: TokenList<'i>,
},
/// An hsl() color.
HSL {
/// The hue component.
h: f32,
/// The saturation component.
s: f32,
/// The lightness component.
l: f32,
/// The unresolved alpha component.
#[cfg_attr(feature = "serde", serde(borrow))]
alpha: TokenList<'i>,
},
/// The light-dark() function.
#[cfg_attr(feature = "serde", serde(rename = "light-dark"))]
LightDark {
/// The light value.
light: TokenList<'i>,
/// The dark value.
dark: TokenList<'i>,
},
}
impl<'i> LightDarkColor for UnresolvedColor<'i> {
#[inline]
fn light_dark(light: Self, dark: Self) -> Self {
UnresolvedColor::LightDark {
light: TokenList(vec![TokenOrValue::UnresolvedColor(light)]),
dark: TokenList(vec![TokenOrValue::UnresolvedColor(dark)]),
}
}
}
impl<'i> UnresolvedColor<'i> {
fn parse<'t>(
f: &CowArcStr<'i>,
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut parser = ComponentParser::new(false);
match_ignore_ascii_case! { &*f,
"rgb" => {
input.parse_nested_block(|input| {
parser.parse_relative::<RGB, _, _>(input, |input, parser| {
let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
if is_legacy {
return Err(input.new_custom_error(ParserError::InvalidValue))
}
input.expect_delim('/')?;
let alpha = TokenList::parse(input, options, 0)?;
Ok(UnresolvedColor::RGB { r, g, b, alpha })
})
})
},
"hsl" => {
input.parse_nested_block(|input| {
parser.parse_relative::<HSL, _, _>(input, |input, parser| {
let (h, s, l, is_legacy) = parse_hsl_hwb_components::<HSL>(input, parser, false)?;
if is_legacy {
return Err(input.new_custom_error(ParserError::InvalidValue))
}
input.expect_delim('/')?;
let alpha = TokenList::parse(input, options, 0)?;
Ok(UnresolvedColor::HSL { h, s, l, alpha })
})
})
},
"light-dark" => {
input.parse_nested_block(|input| {
let light = input.parse_until_before(Delimiter::Comma, |input|
TokenList::parse(input, options, 0)
)?;
input.expect_comma()?;
let dark = TokenList::parse(input, options, 0)?;
Ok(UnresolvedColor::LightDark { light, dark })
})
},
_ => Err(input.new_custom_error(ParserError::InvalidValue))
}
}
fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
UnresolvedColor::RGB { r, g, b, alpha } => {
if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
dest.write_str("rgba(")?;
r.to_css(dest)?;
dest.delim(',', false)?;
g.to_css(dest)?;
dest.delim(',', false)?;
b.to_css(dest)?;
dest.delim(',', false)?;
alpha.to_css(dest, is_custom_property)?;
dest.write_char(')')?;
return Ok(());
}
dest.write_str("rgb(")?;
r.to_css(dest)?;
dest.write_char(' ')?;
g.to_css(dest)?;
dest.write_char(' ')?;
b.to_css(dest)?;
dest.delim('/', true)?;
alpha.to_css(dest, is_custom_property)?;
dest.write_char(')')
}
UnresolvedColor::HSL { h, s, l, alpha } => {
if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
dest.write_str("hsla(")?;
h.to_css(dest)?;
dest.delim(',', false)?;
Percentage(*s / 100.0).to_css(dest)?;
dest.delim(',', false)?;
Percentage(*l / 100.0).to_css(dest)?;
dest.delim(',', false)?;
alpha.to_css(dest, is_custom_property)?;
dest.write_char(')')?;
return Ok(());
}
dest.write_str("hsl(")?;
h.to_css(dest)?;
dest.write_char(' ')?;
Percentage(*s / 100.0).to_css(dest)?;
dest.write_char(' ')?;
Percentage(*l / 100.0).to_css(dest)?;
dest.delim('/', true)?;
alpha.to_css(dest, is_custom_property)?;
dest.write_char(')')
}
UnresolvedColor::LightDark { light, dark } => {
if should_compile!(dest.targets.current, LightDark) {
dest.write_str("var(--lightningcss-light")?;
dest.delim(',', false)?;
light.to_css(dest, is_custom_property)?;
dest.write_char(')')?;
dest.whitespace()?;
dest.write_str("var(--lightningcss-dark")?;
dest.delim(',', false)?;
dark.to_css(dest, is_custom_property)?;
return dest.write_char(')');
}
dest.write_str("light-dark(")?;
light.to_css(dest, is_custom_property)?;
dest.delim(',', false)?;
dark.to_css(dest, is_custom_property)?;
dest.write_char(')')
}
}
}
}