lightningcss/properties/
custom.rs

1//! CSS custom properties and unparsed token values.
2
3use crate::error::{ParserError, PrinterError, PrinterErrorKind};
4use crate::macros::enum_property;
5use crate::prefixes::Feature;
6use crate::printer::Printer;
7use crate::properties::PropertyId;
8use crate::rules::supports::SupportsCondition;
9use crate::stylesheet::ParserOptions;
10use crate::targets::{should_compile, Targets};
11use crate::traits::{Parse, ParseWithOptions, ToCss};
12use crate::values::angle::Angle;
13use crate::values::color::{
14  parse_hsl_hwb_components, parse_rgb_components, ColorFallbackKind, ComponentParser, CssColor, LightDarkColor,
15  HSL, RGBA, SRGB,
16};
17use crate::values::ident::{CustomIdent, DashedIdent, DashedIdentReference, Ident};
18use crate::values::length::{serialize_dimension, LengthValue};
19use crate::values::number::CSSInteger;
20use crate::values::percentage::Percentage;
21use crate::values::resolution::Resolution;
22use crate::values::string::CowArcStr;
23use crate::values::time::Time;
24use crate::values::url::Url;
25#[cfg(feature = "visitor")]
26use crate::visitor::Visit;
27use cssparser::color::parse_hash_color;
28use cssparser::*;
29
30use super::AnimationName;
31#[cfg(feature = "serde")]
32use crate::serialization::ValueWrapper;
33
34/// A CSS custom property, representing any unknown property.
35#[derive(Debug, Clone, PartialEq)]
36#[cfg_attr(feature = "visitor", derive(Visit))]
37#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
40pub struct CustomProperty<'i> {
41  /// The name of the property.
42  #[cfg_attr(feature = "serde", serde(borrow))]
43  pub name: CustomPropertyName<'i>,
44  /// The property value, stored as a raw token list.
45  pub value: TokenList<'i>,
46}
47
48impl<'i> CustomProperty<'i> {
49  /// Parses a custom property with the given name.
50  pub fn parse<'t>(
51    name: CustomPropertyName<'i>,
52    input: &mut Parser<'i, 't>,
53    options: &ParserOptions<'_, 'i>,
54  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
55    let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
56      TokenList::parse(input, options, 0)
57    })?;
58    Ok(CustomProperty { name, value })
59  }
60}
61
62/// A CSS custom property name.
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64#[cfg_attr(feature = "visitor", derive(Visit))]
65#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
66#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
67#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
68pub enum CustomPropertyName<'i> {
69  /// An author-defined CSS custom property.
70  #[cfg_attr(feature = "serde", serde(borrow))]
71  Custom(DashedIdent<'i>),
72  /// An unknown CSS property.
73  Unknown(Ident<'i>),
74}
75
76impl<'i> From<CowArcStr<'i>> for CustomPropertyName<'i> {
77  fn from(name: CowArcStr<'i>) -> Self {
78    if name.starts_with("--") {
79      CustomPropertyName::Custom(DashedIdent(name))
80    } else {
81      CustomPropertyName::Unknown(Ident(name))
82    }
83  }
84}
85
86impl<'i> From<CowRcStr<'i>> for CustomPropertyName<'i> {
87  fn from(name: CowRcStr<'i>) -> Self {
88    CustomPropertyName::from(CowArcStr::from(name))
89  }
90}
91
92impl<'i> AsRef<str> for CustomPropertyName<'i> {
93  #[inline]
94  fn as_ref(&self) -> &str {
95    match self {
96      CustomPropertyName::Custom(c) => c.as_ref(),
97      CustomPropertyName::Unknown(u) => u.as_ref(),
98    }
99  }
100}
101
102impl<'i> ToCss for CustomPropertyName<'i> {
103  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
104  where
105    W: std::fmt::Write,
106  {
107    match self {
108      CustomPropertyName::Custom(c) => c.to_css(dest),
109      CustomPropertyName::Unknown(u) => u.to_css(dest),
110    }
111  }
112}
113
114#[cfg(feature = "serde")]
115#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
116impl<'i, 'de: 'i> serde::Deserialize<'de> for CustomPropertyName<'i> {
117  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118  where
119    D: serde::Deserializer<'de>,
120  {
121    let name = CowArcStr::deserialize(deserializer)?;
122    Ok(name.into())
123  }
124}
125
126/// A known property with an unparsed value.
127///
128/// This type is used when the value of a known property could not
129/// be parsed, e.g. in the case css `var()` references are encountered.
130/// In this case, the raw tokens are stored instead.
131#[derive(Debug, Clone, PartialEq)]
132#[cfg_attr(feature = "visitor", derive(Visit))]
133#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
134#[cfg_attr(
135  feature = "serde",
136  derive(serde::Serialize, serde::Deserialize),
137  serde(rename_all = "camelCase")
138)]
139#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
140pub struct UnparsedProperty<'i> {
141  /// The id of the property.
142  pub property_id: PropertyId<'i>,
143  /// The property value, stored as a raw token list.
144  #[cfg_attr(feature = "serde", serde(borrow))]
145  pub value: TokenList<'i>,
146}
147
148impl<'i> UnparsedProperty<'i> {
149  /// Parses a property with the given id as a token list.
150  pub fn parse<'t>(
151    property_id: PropertyId<'i>,
152    input: &mut Parser<'i, 't>,
153    options: &ParserOptions<'_, 'i>,
154  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
155    let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
156      TokenList::parse(input, options, 0)
157    })?;
158    Ok(UnparsedProperty { property_id, value })
159  }
160
161  pub(crate) fn get_prefixed(&self, targets: Targets, feature: Feature) -> UnparsedProperty<'i> {
162    let mut clone = self.clone();
163    let prefix = self.property_id.prefix();
164    clone.property_id = clone.property_id.with_prefix(targets.prefixes(prefix.or_none(), feature));
165    clone
166  }
167
168  /// Returns a new UnparsedProperty with the same value and the given property id.
169  pub fn with_property_id(&self, property_id: PropertyId<'i>) -> UnparsedProperty<'i> {
170    UnparsedProperty {
171      property_id,
172      value: self.value.clone(),
173    }
174  }
175
176  /// Substitutes variables and re-parses the property.
177  #[cfg(feature = "substitute_variables")]
178  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
179  pub fn substitute_variables<'x>(
180    mut self,
181    vars: &std::collections::HashMap<&str, TokenList<'i>>,
182  ) -> Result<super::Property<'x>, ()> {
183    use super::Property;
184    use crate::stylesheet::PrinterOptions;
185    use static_self::IntoOwned;
186
187    // Substitute variables in the token list.
188    self.value.substitute_variables(vars);
189
190    // Now stringify and re-parse the property to its fully parsed form.
191    // Ideally we'd be able to reuse the tokens rather than printing, but cssparser doesn't provide a way to do that.
192    let mut css = String::new();
193    let mut dest = Printer::new(&mut css, PrinterOptions::default());
194    self.value.to_css(&mut dest, false).unwrap();
195    let property =
196      Property::parse_string(self.property_id.clone(), &css, ParserOptions::default()).map_err(|_| ())?;
197    Ok(property.into_owned())
198  }
199}
200
201/// A raw list of CSS tokens, with embedded parsed values.
202#[derive(Debug, Clone, PartialEq, Eq, Hash)]
203#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_token_list, TOKENS))]
204#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
206#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
207pub struct TokenList<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub Vec<TokenOrValue<'i>>);
208
209/// A raw CSS token, or a parsed value.
210#[derive(Debug, Clone, PartialEq)]
211#[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))]
212#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
213#[cfg_attr(
214  feature = "serde",
215  derive(serde::Serialize, serde::Deserialize),
216  serde(tag = "type", content = "value", rename_all = "kebab-case")
217)]
218#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
219pub enum TokenOrValue<'i> {
220  /// A token.
221  #[cfg_attr(feature = "serde", serde(borrow))]
222  Token(Token<'i>),
223  /// A parsed CSS color.
224  Color(CssColor),
225  /// A color with unresolved components.
226  UnresolvedColor(UnresolvedColor<'i>),
227  /// A parsed CSS url.
228  Url(Url<'i>),
229  /// A CSS variable reference.
230  Var(Variable<'i>),
231  /// A CSS environment variable reference.
232  Env(EnvironmentVariable<'i>),
233  /// A custom CSS function.
234  Function(Function<'i>),
235  /// A length.
236  Length(LengthValue),
237  /// An angle.
238  Angle(Angle),
239  /// A time.
240  Time(Time),
241  /// A resolution.
242  Resolution(Resolution),
243  /// A dashed ident.
244  DashedIdent(DashedIdent<'i>),
245  /// An animation name.
246  AnimationName(AnimationName<'i>),
247}
248
249impl<'i> From<Token<'i>> for TokenOrValue<'i> {
250  fn from(token: Token<'i>) -> TokenOrValue<'i> {
251    TokenOrValue::Token(token)
252  }
253}
254
255impl<'i> TokenOrValue<'i> {
256  /// Returns whether the token is whitespace.
257  pub fn is_whitespace(&self) -> bool {
258    matches!(self, TokenOrValue::Token(Token::WhiteSpace(_)))
259  }
260}
261
262impl<'a> Eq for TokenOrValue<'a> {}
263
264impl<'a> std::hash::Hash for TokenOrValue<'a> {
265  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
266    let tag = std::mem::discriminant(self);
267    tag.hash(state);
268    match self {
269      TokenOrValue::Token(t) => t.hash(state),
270      _ => {
271        // This function is primarily used to deduplicate selectors.
272        // Values inside selectors should be exceedingly rare and implementing
273        // Hash for them is somewhat complex due to floating point values.
274        // For now, we just ignore them, which only means there are more
275        // hash collisions. For such a rare case this is probably fine.
276      }
277    }
278  }
279}
280
281impl<'i> ParseWithOptions<'i> for TokenList<'i> {
282  fn parse_with_options<'t>(
283    input: &mut Parser<'i, 't>,
284    options: &ParserOptions<'_, 'i>,
285  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
286    TokenList::parse(input, options, 0)
287  }
288}
289
290impl<'i> TokenList<'i> {
291  pub(crate) fn parse<'t>(
292    input: &mut Parser<'i, 't>,
293    options: &ParserOptions<'_, 'i>,
294    depth: usize,
295  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
296    let mut tokens = vec![];
297    TokenList::parse_into(input, &mut tokens, options, depth)?;
298
299    // Slice off leading and trailing whitespace if there are at least two tokens.
300    // If there is only one token, we must preserve it. e.g. `--foo: ;` is valid.
301    if tokens.len() >= 2 {
302      let mut slice = &tokens[..];
303      if matches!(tokens.first(), Some(token) if token.is_whitespace()) {
304        slice = &slice[1..];
305      }
306      if matches!(tokens.last(), Some(token) if token.is_whitespace()) {
307        slice = &slice[..slice.len() - 1];
308      }
309      return Ok(TokenList(slice.to_vec()));
310    }
311
312    return Ok(TokenList(tokens));
313  }
314
315  pub(crate) fn parse_raw<'t>(
316    input: &mut Parser<'i, 't>,
317    tokens: &mut Vec<TokenOrValue<'i>>,
318    options: &ParserOptions<'_, 'i>,
319    depth: usize,
320  ) -> Result<(), ParseError<'i, ParserError<'i>>> {
321    if depth > 500 {
322      return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
323    }
324
325    loop {
326      let state = input.state();
327      match input.next_including_whitespace_and_comments() {
328        Ok(token @ &cssparser::Token::ParenthesisBlock)
329        | Ok(token @ &cssparser::Token::SquareBracketBlock)
330        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
331          tokens.push(Token::from(token).into());
332          let closing_delimiter = match token {
333            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
334            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
335            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
336            _ => unreachable!(),
337          };
338
339          input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
340          tokens.push(closing_delimiter.into());
341        }
342        Ok(token @ &cssparser::Token::Function(_)) => {
343          tokens.push(Token::from(token).into());
344          input.parse_nested_block(|input| TokenList::parse_raw(input, tokens, options, depth + 1))?;
345          tokens.push(Token::CloseParenthesis.into());
346        }
347        Ok(token) if token.is_parse_error() => {
348          return Err(ParseError {
349            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
350            location: state.source_location(),
351          })
352        }
353        Ok(token) => {
354          tokens.push(Token::from(token).into());
355        }
356        Err(_) => break,
357      }
358    }
359
360    Ok(())
361  }
362
363  fn parse_into<'t>(
364    input: &mut Parser<'i, 't>,
365    tokens: &mut Vec<TokenOrValue<'i>>,
366    options: &ParserOptions<'_, 'i>,
367    depth: usize,
368  ) -> Result<(), ParseError<'i, ParserError<'i>>> {
369    if depth > 500 {
370      return Err(input.new_custom_error(ParserError::MaximumNestingDepth));
371    }
372
373    let mut last_is_delim = false;
374    let mut last_is_whitespace = false;
375    loop {
376      let state = input.state();
377      match input.next_including_whitespace_and_comments() {
378        Ok(&cssparser::Token::WhiteSpace(..)) | Ok(&cssparser::Token::Comment(..)) => {
379          // Skip whitespace if the last token was a delimiter.
380          // Otherwise, replace all whitespace and comments with a single space character.
381          if !last_is_delim {
382            tokens.push(Token::WhiteSpace(" ".into()).into());
383            last_is_whitespace = true;
384          }
385        }
386        Ok(&cssparser::Token::Function(ref f)) => {
387          // Attempt to parse embedded color values into hex tokens.
388          let f = f.into();
389          if let Some(color) = try_parse_color_token(&f, &state, input) {
390            tokens.push(TokenOrValue::Color(color));
391            last_is_delim = false;
392            last_is_whitespace = false;
393          } else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) {
394            tokens.push(TokenOrValue::UnresolvedColor(color));
395            last_is_delim = true;
396            last_is_whitespace = false;
397          } else if f == "url" {
398            input.reset(&state);
399            tokens.push(TokenOrValue::Url(Url::parse(input)?));
400            last_is_delim = false;
401            last_is_whitespace = false;
402          } else if f == "var" {
403            let var = input.parse_nested_block(|input| {
404              let var = Variable::parse(input, options, depth + 1)?;
405              Ok(TokenOrValue::Var(var))
406            })?;
407            tokens.push(var);
408            last_is_delim = true;
409            last_is_whitespace = false;
410          } else if f == "env" {
411            let env = input.parse_nested_block(|input| {
412              let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?;
413              Ok(TokenOrValue::Env(env))
414            })?;
415            tokens.push(env);
416            last_is_delim = true;
417            last_is_whitespace = false;
418          } else {
419            let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?;
420            tokens.push(TokenOrValue::Function(Function {
421              name: Ident(f),
422              arguments,
423            }));
424            last_is_delim = true; // Whitespace is not required after any of these chars.
425            last_is_whitespace = false;
426          }
427        }
428        Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => {
429          if let Ok((r, g, b, a)) = parse_hash_color(h.as_bytes()) {
430            tokens.push(TokenOrValue::Color(CssColor::RGBA(RGBA::new(r, g, b, a))));
431          } else {
432            tokens.push(Token::Hash(h.into()).into());
433          }
434          last_is_delim = false;
435          last_is_whitespace = false;
436        }
437        Ok(&cssparser::Token::UnquotedUrl(_)) => {
438          input.reset(&state);
439          tokens.push(TokenOrValue::Url(Url::parse(input)?));
440          last_is_delim = false;
441          last_is_whitespace = false;
442        }
443        Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => {
444          tokens.push(TokenOrValue::DashedIdent(name.into()));
445          last_is_delim = false;
446          last_is_whitespace = false;
447        }
448        Ok(token @ &cssparser::Token::ParenthesisBlock)
449        | Ok(token @ &cssparser::Token::SquareBracketBlock)
450        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
451          tokens.push(Token::from(token).into());
452          let closing_delimiter = match token {
453            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
454            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
455            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
456            _ => unreachable!(),
457          };
458
459          input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?;
460
461          tokens.push(closing_delimiter.into());
462          last_is_delim = true; // Whitespace is not required after any of these chars.
463          last_is_whitespace = false;
464        }
465        Ok(token @ cssparser::Token::Dimension { .. }) => {
466          let value = if let Ok(length) = LengthValue::try_from(token) {
467            TokenOrValue::Length(length)
468          } else if let Ok(angle) = Angle::try_from(token) {
469            TokenOrValue::Angle(angle)
470          } else if let Ok(time) = Time::try_from(token) {
471            TokenOrValue::Time(time)
472          } else if let Ok(resolution) = Resolution::try_from(token) {
473            TokenOrValue::Resolution(resolution)
474          } else {
475            TokenOrValue::Token(token.into())
476          };
477          tokens.push(value);
478          last_is_delim = false;
479          last_is_whitespace = false;
480        }
481        Ok(token) if token.is_parse_error() => {
482          return Err(ParseError {
483            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
484            location: state.source_location(),
485          })
486        }
487        Ok(token) => {
488          last_is_delim = matches!(token, cssparser::Token::Delim(_) | cssparser::Token::Comma);
489
490          // If this is a delimiter, and the last token was whitespace,
491          // replace the whitespace with the delimiter since both are not required.
492          if last_is_delim && last_is_whitespace {
493            let last = tokens.last_mut().unwrap();
494            *last = Token::from(token).into();
495          } else {
496            tokens.push(Token::from(token).into());
497          }
498
499          last_is_whitespace = false;
500        }
501        Err(_) => break,
502      }
503    }
504
505    Ok(())
506  }
507}
508
509#[inline]
510fn try_parse_color_token<'i, 't>(
511  f: &CowArcStr<'i>,
512  state: &ParserState,
513  input: &mut Parser<'i, 't>,
514) -> Option<CssColor> {
515  match_ignore_ascii_case! { &*f,
516    "rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" => {
517      let s = input.state();
518      input.reset(&state);
519      if let Ok(color) = CssColor::parse(input) {
520        return Some(color)
521      }
522      input.reset(&s);
523    },
524    _ => {}
525  }
526
527  None
528}
529
530impl<'i> TokenList<'i> {
531  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
532  where
533    W: std::fmt::Write,
534  {
535    if !dest.minify && self.0.len() == 1 && matches!(self.0.first(), Some(token) if token.is_whitespace()) {
536      return Ok(());
537    }
538
539    let mut has_whitespace = false;
540    for (i, token_or_value) in self.0.iter().enumerate() {
541      has_whitespace = match token_or_value {
542        TokenOrValue::Color(color) => {
543          color.to_css(dest)?;
544          false
545        }
546        TokenOrValue::UnresolvedColor(color) => {
547          color.to_css(dest, is_custom_property)?;
548          false
549        }
550        TokenOrValue::Url(url) => {
551          if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() {
552            return Err(dest.error(
553              PrinterErrorKind::AmbiguousUrlInCustomProperty {
554                url: url.url.as_ref().to_owned(),
555              },
556              url.loc,
557            ));
558          }
559          url.to_css(dest)?;
560          false
561        }
562        TokenOrValue::Var(var) => {
563          var.to_css(dest, is_custom_property)?;
564          self.write_whitespace_if_needed(i, dest)?
565        }
566        TokenOrValue::Env(env) => {
567          env.to_css(dest, is_custom_property)?;
568          self.write_whitespace_if_needed(i, dest)?
569        }
570        TokenOrValue::Function(f) => {
571          f.to_css(dest, is_custom_property)?;
572          self.write_whitespace_if_needed(i, dest)?
573        }
574        TokenOrValue::Length(v) => {
575          // Do not serialize unitless zero lengths in custom properties as it may break calc().
576          let (value, unit) = v.to_unit_value();
577          serialize_dimension(value, unit, dest)?;
578          false
579        }
580        TokenOrValue::Angle(v) => {
581          v.to_css(dest)?;
582          false
583        }
584        TokenOrValue::Time(v) => {
585          v.to_css(dest)?;
586          false
587        }
588        TokenOrValue::Resolution(v) => {
589          v.to_css(dest)?;
590          false
591        }
592        TokenOrValue::DashedIdent(v) => {
593          v.to_css(dest)?;
594          false
595        }
596        TokenOrValue::AnimationName(v) => {
597          v.to_css(dest)?;
598          false
599        }
600        TokenOrValue::Token(token) => match token {
601          Token::Delim(d) => {
602            if *d == '+' || *d == '-' {
603              dest.write_char(' ')?;
604              dest.write_char(*d)?;
605              dest.write_char(' ')?;
606            } else {
607              let ws_before = !has_whitespace && (*d == '/' || *d == '*');
608              dest.delim(*d, ws_before)?;
609            }
610            true
611          }
612          Token::Comma => {
613            dest.delim(',', false)?;
614            true
615          }
616          Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => {
617            token.to_css(dest)?;
618            self.write_whitespace_if_needed(i, dest)?
619          }
620          Token::Dimension { value, unit, .. } => {
621            serialize_dimension(*value, unit, dest)?;
622            false
623          }
624          Token::Number { value, .. } => {
625            value.to_css(dest)?;
626            false
627          }
628          _ => {
629            token.to_css(dest)?;
630            matches!(token, Token::WhiteSpace(..))
631          }
632        },
633      };
634    }
635
636    Ok(())
637  }
638
639  pub(crate) fn to_css_raw<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
640  where
641    W: std::fmt::Write,
642  {
643    for token_or_value in &self.0 {
644      match token_or_value {
645        TokenOrValue::Token(token) => {
646          token.to_css(dest)?;
647        }
648        _ => {
649          return Err(PrinterError {
650            kind: PrinterErrorKind::FmtError,
651            loc: None,
652          })
653        }
654      }
655    }
656
657    Ok(())
658  }
659
660  #[inline]
661  fn write_whitespace_if_needed<W>(&self, i: usize, dest: &mut Printer<W>) -> Result<bool, PrinterError>
662  where
663    W: std::fmt::Write,
664  {
665    if !dest.minify
666      && i != self.0.len() - 1
667      && !matches!(
668        self.0[i + 1],
669        TokenOrValue::Token(Token::Comma) | TokenOrValue::Token(Token::CloseParenthesis)
670      )
671    {
672      // Whitespace is removed during parsing, so add it back if we aren't minifying.
673      dest.write_char(' ')?;
674      Ok(true)
675    } else {
676      Ok(false)
677    }
678  }
679}
680
681/// A raw CSS token.
682// Copied from cssparser to change CowRcStr to CowArcStr
683#[derive(Debug, Clone, PartialEq)]
684#[cfg_attr(feature = "visitor", derive(Visit))]
685#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
686#[cfg_attr(
687  feature = "serde",
688  derive(serde::Serialize, serde::Deserialize),
689  serde(tag = "type", rename_all = "kebab-case")
690)]
691#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
692pub enum Token<'a> {
693  /// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram)
694  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
695  Ident(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'a>),
696
697  /// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
698  ///
699  /// The value does not include the `@` marker.
700  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
701  AtKeyword(CowArcStr<'a>),
702
703  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted"
704  ///
705  /// The value does not include the `#` marker.
706  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
707  Hash(CowArcStr<'a>),
708
709  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id"
710  ///
711  /// The value does not include the `#` marker.
712  #[cfg_attr(feature = "serde", serde(rename = "id-hash", with = "ValueWrapper::<CowArcStr>"))]
713  IDHash(CowArcStr<'a>), // Hash that is a valid ID selector.
714
715  /// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram)
716  ///
717  /// The value does not include the quotes.
718  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
719  String(CowArcStr<'a>),
720
721  /// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram)
722  ///
723  /// The value does not include the `url(` `)` markers.  Note that `url( <string-token> )` is represented by a
724  /// `Function` token.
725  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
726  UnquotedUrl(CowArcStr<'a>),
727
728  /// A `<delim-token>`
729  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<char>"))]
730  Delim(char),
731
732  /// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram)
733  Number {
734    /// Whether the number had a `+` or `-` sign.
735    ///
736    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
737    #[cfg_attr(feature = "serde", serde(skip))]
738    has_sign: bool,
739
740    /// The value as a float
741    value: f32,
742
743    /// If the origin source did not include a fractional part, the value as an integer.
744    #[cfg_attr(feature = "serde", serde(skip))]
745    int_value: Option<i32>,
746  },
747
748  /// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
749  Percentage {
750    /// Whether the number had a `+` or `-` sign.
751    #[cfg_attr(feature = "serde", serde(skip))]
752    has_sign: bool,
753
754    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
755    #[cfg_attr(feature = "serde", serde(rename = "value"))]
756    unit_value: f32,
757
758    /// If the origin source did not include a fractional part, the value as an integer.
759    /// It is **not** divided by 100.
760    #[cfg_attr(feature = "serde", serde(skip))]
761    int_value: Option<i32>,
762  },
763
764  /// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
765  Dimension {
766    /// Whether the number had a `+` or `-` sign.
767    ///
768    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
769    #[cfg_attr(feature = "serde", serde(skip))]
770    has_sign: bool,
771
772    /// The value as a float
773    value: f32,
774
775    /// If the origin source did not include a fractional part, the value as an integer.
776    #[cfg_attr(feature = "serde", serde(skip))]
777    int_value: Option<i32>,
778
779    /// The unit, e.g. "px" in `12px`
780    unit: CowArcStr<'a>,
781  },
782
783  /// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
784  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
785  WhiteSpace(CowArcStr<'a>),
786
787  /// A comment.
788  ///
789  /// The CSS Syntax spec does not generate tokens for comments,
790  /// But we do, because we can (borrowed &str makes it cheap).
791  ///
792  /// The value does not include the `/*` `*/` markers.
793  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
794  Comment(CowArcStr<'a>),
795
796  /// A `:` `<colon-token>`
797  Colon, // :
798
799  /// A `;` `<semicolon-token>`
800  Semicolon, // ;
801
802  /// A `,` `<comma-token>`
803  Comma, // ,
804
805  /// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram)
806  IncludeMatch,
807
808  /// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram)
809  DashMatch,
810
811  /// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram)
812  PrefixMatch,
813
814  /// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram)
815  SuffixMatch,
816
817  /// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
818  SubstringMatch,
819
820  /// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram)
821  #[cfg_attr(feature = "serde", serde(rename = "cdo"))]
822  CDO,
823
824  /// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram)
825  #[cfg_attr(feature = "serde", serde(rename = "cdc"))]
826  CDC,
827
828  /// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram)
829  ///
830  /// The value (name) does not include the `(` marker.
831  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
832  Function(CowArcStr<'a>),
833
834  /// A `<(-token>`
835  ParenthesisBlock,
836
837  /// A `<[-token>`
838  SquareBracketBlock,
839
840  /// A `<{-token>`
841  CurlyBracketBlock,
842
843  /// A `<bad-url-token>`
844  ///
845  /// This token always indicates a parse error.
846  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
847  BadUrl(CowArcStr<'a>),
848
849  /// A `<bad-string-token>`
850  ///
851  /// This token always indicates a parse error.
852  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
853  BadString(CowArcStr<'a>),
854
855  /// A `<)-token>`
856  ///
857  /// When obtained from one of the `Parser::next*` methods,
858  /// this token is always unmatched and indicates a parse error.
859  CloseParenthesis,
860
861  /// A `<]-token>`
862  ///
863  /// When obtained from one of the `Parser::next*` methods,
864  /// this token is always unmatched and indicates a parse error.
865  CloseSquareBracket,
866
867  /// A `<}-token>`
868  ///
869  /// When obtained from one of the `Parser::next*` methods,
870  /// this token is always unmatched and indicates a parse error.
871  CloseCurlyBracket,
872}
873
874impl<'a> From<&cssparser::Token<'a>> for Token<'a> {
875  #[inline]
876  fn from(t: &cssparser::Token<'a>) -> Token<'a> {
877    match t {
878      cssparser::Token::Ident(x) => Token::Ident(x.into()),
879      cssparser::Token::AtKeyword(x) => Token::AtKeyword(x.into()),
880      cssparser::Token::Hash(x) => Token::Hash(x.into()),
881      cssparser::Token::IDHash(x) => Token::IDHash(x.into()),
882      cssparser::Token::QuotedString(x) => Token::String(x.into()),
883      cssparser::Token::UnquotedUrl(x) => Token::UnquotedUrl(x.into()),
884      cssparser::Token::Function(x) => Token::Function(x.into()),
885      cssparser::Token::BadUrl(x) => Token::BadUrl(x.into()),
886      cssparser::Token::BadString(x) => Token::BadString(x.into()),
887      cssparser::Token::Delim(c) => Token::Delim(*c),
888      cssparser::Token::Number {
889        has_sign,
890        value,
891        int_value,
892      } => Token::Number {
893        has_sign: *has_sign,
894        value: *value,
895        int_value: *int_value,
896      },
897      cssparser::Token::Dimension {
898        has_sign,
899        value,
900        int_value,
901        unit,
902      } => Token::Dimension {
903        has_sign: *has_sign,
904        value: *value,
905        int_value: *int_value,
906        unit: unit.into(),
907      },
908      cssparser::Token::Percentage {
909        has_sign,
910        unit_value,
911        int_value,
912      } => Token::Percentage {
913        has_sign: *has_sign,
914        unit_value: *unit_value,
915        int_value: *int_value,
916      },
917      cssparser::Token::WhiteSpace(w) => Token::WhiteSpace((*w).into()),
918      cssparser::Token::Comment(c) => Token::Comment((*c).into()),
919      cssparser::Token::Colon => Token::Colon,
920      cssparser::Token::Semicolon => Token::Semicolon,
921      cssparser::Token::Comma => Token::Comma,
922      cssparser::Token::IncludeMatch => Token::IncludeMatch,
923      cssparser::Token::DashMatch => Token::DashMatch,
924      cssparser::Token::PrefixMatch => Token::PrefixMatch,
925      cssparser::Token::SuffixMatch => Token::SuffixMatch,
926      cssparser::Token::SubstringMatch => Token::SubstringMatch,
927      cssparser::Token::CDO => Token::CDO,
928      cssparser::Token::CDC => Token::CDC,
929      cssparser::Token::ParenthesisBlock => Token::ParenthesisBlock,
930      cssparser::Token::SquareBracketBlock => Token::SquareBracketBlock,
931      cssparser::Token::CurlyBracketBlock => Token::CurlyBracketBlock,
932      cssparser::Token::CloseParenthesis => Token::CloseParenthesis,
933      cssparser::Token::CloseSquareBracket => Token::CloseSquareBracket,
934      cssparser::Token::CloseCurlyBracket => Token::CloseCurlyBracket,
935    }
936  }
937}
938
939impl<'a> ToCss for Token<'a> {
940  #[inline]
941  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
942  where
943    W: std::fmt::Write,
944  {
945    use cssparser::ToCss;
946    match self {
947      Token::Ident(x) => cssparser::Token::Ident(x.as_ref().into()).to_css(dest)?,
948      Token::AtKeyword(x) => cssparser::Token::AtKeyword(x.as_ref().into()).to_css(dest)?,
949      Token::Hash(x) => cssparser::Token::Hash(x.as_ref().into()).to_css(dest)?,
950      Token::IDHash(x) => cssparser::Token::IDHash(x.as_ref().into()).to_css(dest)?,
951      Token::String(x) => cssparser::Token::QuotedString(x.as_ref().into()).to_css(dest)?,
952      Token::UnquotedUrl(x) => cssparser::Token::UnquotedUrl(x.as_ref().into()).to_css(dest)?,
953      Token::Function(x) => cssparser::Token::Function(x.as_ref().into()).to_css(dest)?,
954      Token::BadUrl(x) => cssparser::Token::BadUrl(x.as_ref().into()).to_css(dest)?,
955      Token::BadString(x) => cssparser::Token::BadString(x.as_ref().into()).to_css(dest)?,
956      Token::Delim(c) => cssparser::Token::Delim(*c).to_css(dest)?,
957      Token::Number {
958        has_sign,
959        value,
960        int_value,
961      } => cssparser::Token::Number {
962        has_sign: *has_sign,
963        value: *value,
964        int_value: *int_value,
965      }
966      .to_css(dest)?,
967      Token::Dimension {
968        has_sign,
969        value,
970        int_value,
971        unit,
972      } => cssparser::Token::Dimension {
973        has_sign: *has_sign,
974        value: *value,
975        int_value: *int_value,
976        unit: unit.as_ref().into(),
977      }
978      .to_css(dest)?,
979      Token::Percentage {
980        has_sign,
981        unit_value,
982        int_value,
983      } => cssparser::Token::Percentage {
984        has_sign: *has_sign,
985        unit_value: *unit_value,
986        int_value: *int_value,
987      }
988      .to_css(dest)?,
989      Token::WhiteSpace(w) => cssparser::Token::WhiteSpace(w).to_css(dest)?,
990      Token::Comment(c) => cssparser::Token::Comment(c).to_css(dest)?,
991      Token::Colon => cssparser::Token::Colon.to_css(dest)?,
992      Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?,
993      Token::Comma => cssparser::Token::Comma.to_css(dest)?,
994      Token::IncludeMatch => cssparser::Token::IncludeMatch.to_css(dest)?,
995      Token::DashMatch => cssparser::Token::DashMatch.to_css(dest)?,
996      Token::PrefixMatch => cssparser::Token::PrefixMatch.to_css(dest)?,
997      Token::SuffixMatch => cssparser::Token::SuffixMatch.to_css(dest)?,
998      Token::SubstringMatch => cssparser::Token::SubstringMatch.to_css(dest)?,
999      Token::CDO => cssparser::Token::CDO.to_css(dest)?,
1000      Token::CDC => cssparser::Token::CDC.to_css(dest)?,
1001      Token::ParenthesisBlock => cssparser::Token::ParenthesisBlock.to_css(dest)?,
1002      Token::SquareBracketBlock => cssparser::Token::SquareBracketBlock.to_css(dest)?,
1003      Token::CurlyBracketBlock => cssparser::Token::CurlyBracketBlock.to_css(dest)?,
1004      Token::CloseParenthesis => cssparser::Token::CloseParenthesis.to_css(dest)?,
1005      Token::CloseSquareBracket => cssparser::Token::CloseSquareBracket.to_css(dest)?,
1006      Token::CloseCurlyBracket => cssparser::Token::CloseCurlyBracket.to_css(dest)?,
1007    }
1008
1009    Ok(())
1010  }
1011}
1012
1013impl<'a> Eq for Token<'a> {}
1014
1015impl<'a> std::hash::Hash for Token<'a> {
1016  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1017    let tag = std::mem::discriminant(self);
1018    tag.hash(state);
1019    match self {
1020      Token::Ident(x) => x.hash(state),
1021      Token::AtKeyword(x) => x.hash(state),
1022      Token::Hash(x) => x.hash(state),
1023      Token::IDHash(x) => x.hash(state),
1024      Token::String(x) => x.hash(state),
1025      Token::UnquotedUrl(x) => x.hash(state),
1026      Token::Function(x) => x.hash(state),
1027      Token::BadUrl(x) => x.hash(state),
1028      Token::BadString(x) => x.hash(state),
1029      Token::Delim(x) => x.hash(state),
1030      Token::Number {
1031        has_sign,
1032        value,
1033        int_value,
1034      } => {
1035        has_sign.hash(state);
1036        integer_decode(*value).hash(state);
1037        int_value.hash(state);
1038      }
1039      Token::Dimension {
1040        has_sign,
1041        value,
1042        int_value,
1043        unit,
1044      } => {
1045        has_sign.hash(state);
1046        integer_decode(*value).hash(state);
1047        int_value.hash(state);
1048        unit.hash(state);
1049      }
1050      Token::Percentage {
1051        has_sign,
1052        unit_value,
1053        int_value,
1054      } => {
1055        has_sign.hash(state);
1056        integer_decode(*unit_value).hash(state);
1057        int_value.hash(state);
1058      }
1059      Token::WhiteSpace(w) => w.hash(state),
1060      Token::Comment(c) => c.hash(state),
1061      Token::Colon
1062      | Token::Semicolon
1063      | Token::Comma
1064      | Token::IncludeMatch
1065      | Token::DashMatch
1066      | Token::PrefixMatch
1067      | Token::SuffixMatch
1068      | Token::SubstringMatch
1069      | Token::CDO
1070      | Token::CDC
1071      | Token::ParenthesisBlock
1072      | Token::SquareBracketBlock
1073      | Token::CurlyBracketBlock
1074      | Token::CloseParenthesis
1075      | Token::CloseSquareBracket
1076      | Token::CloseCurlyBracket => {}
1077    }
1078  }
1079}
1080
1081/// Converts a floating point value into its mantissa, exponent,
1082/// and sign components so that it can be hashed.
1083fn integer_decode(v: f32) -> (u32, i16, i8) {
1084  let bits: u32 = unsafe { std::mem::transmute(v) };
1085  let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 };
1086  let mut exponent: i16 = ((bits >> 23) & 0xff) as i16;
1087  let mantissa = if exponent == 0 {
1088    (bits & 0x7fffff) << 1
1089  } else {
1090    (bits & 0x7fffff) | 0x800000
1091  };
1092  // Exponent bias + mantissa shift
1093  exponent -= 127 + 23;
1094  (mantissa, exponent, sign)
1095}
1096
1097impl<'i> TokenList<'i> {
1098  pub(crate) fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
1099    let mut fallbacks = ColorFallbackKind::empty();
1100    for token in &self.0 {
1101      match token {
1102        TokenOrValue::Color(color) => {
1103          fallbacks |= color.get_possible_fallbacks(targets);
1104        }
1105        TokenOrValue::Function(f) => {
1106          fallbacks |= f.arguments.get_necessary_fallbacks(targets);
1107        }
1108        TokenOrValue::Var(v) => {
1109          if let Some(fallback) = &v.fallback {
1110            fallbacks |= fallback.get_necessary_fallbacks(targets);
1111          }
1112        }
1113        TokenOrValue::Env(v) => {
1114          if let Some(fallback) = &v.fallback {
1115            fallbacks |= fallback.get_necessary_fallbacks(targets);
1116          }
1117        }
1118        _ => {}
1119      }
1120    }
1121
1122    fallbacks
1123  }
1124
1125  pub(crate) fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1126    let tokens = self
1127      .0
1128      .iter()
1129      .map(|token| match token {
1130        TokenOrValue::Color(color) => TokenOrValue::Color(color.get_fallback(kind)),
1131        TokenOrValue::Function(f) => TokenOrValue::Function(f.get_fallback(kind)),
1132        TokenOrValue::Var(v) => TokenOrValue::Var(v.get_fallback(kind)),
1133        TokenOrValue::Env(e) => TokenOrValue::Env(e.get_fallback(kind)),
1134        _ => token.clone(),
1135      })
1136      .collect();
1137    TokenList(tokens)
1138  }
1139
1140  pub(crate) fn get_fallbacks(&mut self, targets: Targets) -> Vec<(SupportsCondition<'i>, Self)> {
1141    // Get the full list of possible fallbacks, and remove the lowest one, which will replace
1142    // the original declaration. The remaining fallbacks need to be added as @supports rules.
1143    let mut fallbacks = self.get_necessary_fallbacks(targets);
1144    let lowest_fallback = fallbacks.lowest();
1145    fallbacks.remove(lowest_fallback);
1146
1147    let mut res = Vec::new();
1148    if fallbacks.contains(ColorFallbackKind::P3) {
1149      res.push((
1150        ColorFallbackKind::P3.supports_condition(),
1151        self.get_fallback(ColorFallbackKind::P3),
1152      ));
1153    }
1154
1155    if fallbacks.contains(ColorFallbackKind::LAB) {
1156      res.push((
1157        ColorFallbackKind::LAB.supports_condition(),
1158        self.get_fallback(ColorFallbackKind::LAB),
1159      ));
1160    }
1161
1162    if !lowest_fallback.is_empty() {
1163      for token in self.0.iter_mut() {
1164        match token {
1165          TokenOrValue::Color(color) => {
1166            *color = color.get_fallback(lowest_fallback);
1167          }
1168          TokenOrValue::Function(f) => *f = f.get_fallback(lowest_fallback),
1169          TokenOrValue::Var(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
1170          TokenOrValue::Env(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
1171          _ => {}
1172        }
1173      }
1174    }
1175
1176    res
1177  }
1178
1179  /// Substitutes variables with the provided values.
1180  #[cfg(feature = "substitute_variables")]
1181  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
1182  pub fn substitute_variables(&mut self, vars: &std::collections::HashMap<&str, TokenList<'i>>) {
1183    self.visit(&mut VarInliner { vars }).unwrap()
1184  }
1185}
1186
1187#[cfg(feature = "substitute_variables")]
1188struct VarInliner<'a, 'i> {
1189  vars: &'a std::collections::HashMap<&'a str, TokenList<'i>>,
1190}
1191
1192#[cfg(feature = "substitute_variables")]
1193impl<'a, 'i> crate::visitor::Visitor<'i> for VarInliner<'a, 'i> {
1194  type Error = std::convert::Infallible;
1195
1196  fn visit_types(&self) -> crate::visitor::VisitTypes {
1197    crate::visit_types!(TOKENS | VARIABLES)
1198  }
1199
1200  fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) -> Result<(), Self::Error> {
1201    let mut i = 0;
1202    let mut seen = std::collections::HashSet::new();
1203    while i < tokens.0.len() {
1204      let token = &mut tokens.0[i];
1205      token.visit(self).unwrap();
1206      if let TokenOrValue::Var(var) = token {
1207        if let Some(value) = self.vars.get(var.name.ident.0.as_ref()) {
1208          // Ignore circular references.
1209          if seen.insert(var.name.ident.0.clone()) {
1210            tokens.0.splice(i..i + 1, value.0.iter().cloned());
1211            // Don't advance. We need to replace any variables in the value.
1212            continue;
1213          }
1214        } else if let Some(fallback) = &var.fallback {
1215          let fallback = fallback.0.clone();
1216          if seen.insert(var.name.ident.0.clone()) {
1217            tokens.0.splice(i..i + 1, fallback.into_iter());
1218            continue;
1219          }
1220        }
1221      }
1222      seen.clear();
1223      i += 1;
1224    }
1225    Ok(())
1226  }
1227}
1228
1229/// A CSS variable reference.
1230#[derive(Debug, Clone, PartialEq)]
1231#[cfg_attr(feature = "visitor", derive(Visit))]
1232#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1233#[cfg_attr(feature = "visitor", visit(visit_variable, VARIABLES))]
1234#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1235#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1236pub struct Variable<'i> {
1237  /// The variable name.
1238  #[cfg_attr(feature = "serde", serde(borrow))]
1239  pub name: DashedIdentReference<'i>,
1240  /// A fallback value in case the variable is not defined.
1241  pub fallback: Option<TokenList<'i>>,
1242}
1243
1244impl<'i> Variable<'i> {
1245  fn parse<'t>(
1246    input: &mut Parser<'i, 't>,
1247    options: &ParserOptions<'_, 'i>,
1248    depth: usize,
1249  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1250    let name = DashedIdentReference::parse_with_options(input, options)?;
1251
1252    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
1253      Some(TokenList::parse(input, options, depth)?)
1254    } else {
1255      None
1256    };
1257
1258    Ok(Variable { name, fallback })
1259  }
1260
1261  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1262  where
1263    W: std::fmt::Write,
1264  {
1265    dest.write_str("var(")?;
1266    self.name.to_css(dest)?;
1267    if let Some(fallback) = &self.fallback {
1268      dest.delim(',', false)?;
1269      fallback.to_css(dest, is_custom_property)?;
1270    }
1271    dest.write_char(')')
1272  }
1273
1274  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1275    Variable {
1276      name: self.name.clone(),
1277      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
1278    }
1279  }
1280}
1281
1282/// A CSS environment variable reference.
1283#[derive(Debug, Clone, PartialEq)]
1284#[cfg_attr(
1285  feature = "visitor",
1286  derive(Visit),
1287  visit(visit_environment_variable, ENVIRONMENT_VARIABLES)
1288)]
1289#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1290#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1291#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1292pub struct EnvironmentVariable<'i> {
1293  /// The environment variable name.
1294  #[cfg_attr(feature = "serde", serde(borrow))]
1295  pub name: EnvironmentVariableName<'i>,
1296  /// Optional indices into the dimensions of the environment variable.
1297  #[cfg_attr(feature = "serde", serde(default))]
1298  pub indices: Vec<CSSInteger>,
1299  /// A fallback value in case the variable is not defined.
1300  pub fallback: Option<TokenList<'i>>,
1301}
1302
1303/// A CSS environment variable name.
1304#[derive(Debug, Clone, PartialEq)]
1305#[cfg_attr(feature = "visitor", derive(Visit))]
1306#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1307#[cfg_attr(
1308  feature = "serde",
1309  derive(serde::Serialize, serde::Deserialize),
1310  serde(tag = "type", rename_all = "lowercase")
1311)]
1312#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1313pub enum EnvironmentVariableName<'i> {
1314  /// A UA-defined environment variable.
1315  #[cfg_attr(
1316    feature = "serde",
1317    serde(with = "crate::serialization::ValueWrapper::<UAEnvironmentVariable>")
1318  )]
1319  UA(UAEnvironmentVariable),
1320  /// A custom author-defined environment variable.
1321  #[cfg_attr(feature = "serde", serde(borrow))]
1322  Custom(DashedIdentReference<'i>),
1323  /// An unknown environment variable.
1324  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CustomIdent>"))]
1325  Unknown(CustomIdent<'i>),
1326}
1327
1328enum_property! {
1329  /// A UA-defined environment variable name.
1330  pub enum UAEnvironmentVariable {
1331    /// The safe area inset from the top of the viewport.
1332    SafeAreaInsetTop,
1333    /// The safe area inset from the right of the viewport.
1334    SafeAreaInsetRight,
1335    /// The safe area inset from the bottom of the viewport.
1336    SafeAreaInsetBottom,
1337    /// The safe area inset from the left of the viewport.
1338    SafeAreaInsetLeft,
1339    /// The viewport segment width.
1340    ViewportSegmentWidth,
1341    /// The viewport segment height.
1342    ViewportSegmentHeight,
1343    /// The viewport segment top position.
1344    ViewportSegmentTop,
1345    /// The viewport segment left position.
1346    ViewportSegmentLeft,
1347    /// The viewport segment bottom position.
1348    ViewportSegmentBottom,
1349    /// The viewport segment right position.
1350    ViewportSegmentRight,
1351  }
1352}
1353
1354impl<'i> EnvironmentVariableName<'i> {
1355  /// Returns the name of the environment variable as a string.
1356  pub fn name(&self) -> &str {
1357    match self {
1358      EnvironmentVariableName::UA(ua) => ua.as_str(),
1359      EnvironmentVariableName::Custom(c) => c.ident.as_ref(),
1360      EnvironmentVariableName::Unknown(u) => u.0.as_ref(),
1361    }
1362  }
1363}
1364
1365impl<'i> Parse<'i> for EnvironmentVariableName<'i> {
1366  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1367    if let Ok(ua) = input.try_parse(UAEnvironmentVariable::parse) {
1368      return Ok(EnvironmentVariableName::UA(ua));
1369    }
1370
1371    if let Ok(dashed) =
1372      input.try_parse(|input| DashedIdentReference::parse_with_options(input, &ParserOptions::default()))
1373    {
1374      return Ok(EnvironmentVariableName::Custom(dashed));
1375    }
1376
1377    let ident = CustomIdent::parse(input)?;
1378    return Ok(EnvironmentVariableName::Unknown(ident));
1379  }
1380}
1381
1382impl<'i> ToCss for EnvironmentVariableName<'i> {
1383  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1384  where
1385    W: std::fmt::Write,
1386  {
1387    match self {
1388      EnvironmentVariableName::UA(ua) => ua.to_css(dest),
1389      EnvironmentVariableName::Custom(custom) => custom.to_css(dest),
1390      EnvironmentVariableName::Unknown(unknown) => unknown.to_css(dest),
1391    }
1392  }
1393}
1394
1395impl<'i> EnvironmentVariable<'i> {
1396  pub(crate) fn parse<'t>(
1397    input: &mut Parser<'i, 't>,
1398    options: &ParserOptions<'_, 'i>,
1399    depth: usize,
1400  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1401    input.expect_function_matching("env")?;
1402    input.parse_nested_block(|input| Self::parse_nested(input, options, depth))
1403  }
1404
1405  pub(crate) fn parse_nested<'t>(
1406    input: &mut Parser<'i, 't>,
1407    options: &ParserOptions<'_, 'i>,
1408    depth: usize,
1409  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1410    let name = EnvironmentVariableName::parse(input)?;
1411    let mut indices = Vec::new();
1412    while let Ok(index) = input.try_parse(CSSInteger::parse) {
1413      indices.push(index);
1414    }
1415
1416    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
1417      Some(TokenList::parse(input, options, depth + 1)?)
1418    } else {
1419      None
1420    };
1421
1422    Ok(EnvironmentVariable {
1423      name,
1424      indices,
1425      fallback,
1426    })
1427  }
1428
1429  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1430  where
1431    W: std::fmt::Write,
1432  {
1433    dest.write_str("env(")?;
1434    self.name.to_css(dest)?;
1435
1436    for item in &self.indices {
1437      dest.write_char(' ')?;
1438      item.to_css(dest)?;
1439    }
1440
1441    if let Some(fallback) = &self.fallback {
1442      dest.delim(',', false)?;
1443      fallback.to_css(dest, is_custom_property)?;
1444    }
1445    dest.write_char(')')
1446  }
1447
1448  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1449    EnvironmentVariable {
1450      name: self.name.clone(),
1451      indices: self.indices.clone(),
1452      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
1453    }
1454  }
1455}
1456
1457/// A custom CSS function.
1458#[derive(Debug, Clone, PartialEq)]
1459#[cfg_attr(feature = "visitor", derive(Visit))]
1460#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1461#[cfg_attr(feature = "visitor", visit(visit_function, FUNCTIONS))]
1462#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1463#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1464pub struct Function<'i> {
1465  /// The function name.
1466  #[cfg_attr(feature = "serde", serde(borrow))]
1467  pub name: Ident<'i>,
1468  /// The function arguments.
1469  pub arguments: TokenList<'i>,
1470}
1471
1472impl<'i> Function<'i> {
1473  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1474  where
1475    W: std::fmt::Write,
1476  {
1477    self.name.to_css(dest)?;
1478    dest.write_char('(')?;
1479    self.arguments.to_css(dest, is_custom_property)?;
1480    dest.write_char(')')
1481  }
1482
1483  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1484    Function {
1485      name: self.name.clone(),
1486      arguments: self.arguments.get_fallback(kind),
1487    }
1488  }
1489}
1490
1491/// A color value with an unresolved alpha value (e.g. a variable).
1492/// These can be converted from the modern slash syntax to older comma syntax.
1493/// This can only be done when the only unresolved component is the alpha
1494/// since variables can resolve to multiple tokens.
1495#[derive(Debug, Clone, PartialEq)]
1496#[cfg_attr(feature = "visitor", derive(Visit))]
1497#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1498#[cfg_attr(
1499  feature = "serde",
1500  derive(serde::Serialize, serde::Deserialize),
1501  serde(tag = "type", rename_all = "lowercase")
1502)]
1503#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1504pub enum UnresolvedColor<'i> {
1505  /// An rgb() color.
1506  RGB {
1507    /// The red component.
1508    r: f32,
1509    /// The green component.
1510    g: f32,
1511    /// The blue component.
1512    b: f32,
1513    /// The unresolved alpha component.
1514    #[cfg_attr(feature = "serde", serde(borrow))]
1515    alpha: TokenList<'i>,
1516  },
1517  /// An hsl() color.
1518  HSL {
1519    /// The hue component.
1520    h: f32,
1521    /// The saturation component.
1522    s: f32,
1523    /// The lightness component.
1524    l: f32,
1525    /// The unresolved alpha component.
1526    #[cfg_attr(feature = "serde", serde(borrow))]
1527    alpha: TokenList<'i>,
1528  },
1529  /// The light-dark() function.
1530  #[cfg_attr(feature = "serde", serde(rename = "light-dark"))]
1531  LightDark {
1532    /// The light value.
1533    light: TokenList<'i>,
1534    /// The dark value.
1535    dark: TokenList<'i>,
1536  },
1537}
1538
1539impl<'i> LightDarkColor for UnresolvedColor<'i> {
1540  #[inline]
1541  fn light_dark(light: Self, dark: Self) -> Self {
1542    UnresolvedColor::LightDark {
1543      light: TokenList(vec![TokenOrValue::UnresolvedColor(light)]),
1544      dark: TokenList(vec![TokenOrValue::UnresolvedColor(dark)]),
1545    }
1546  }
1547}
1548
1549impl<'i> UnresolvedColor<'i> {
1550  fn parse<'t>(
1551    f: &CowArcStr<'i>,
1552    input: &mut Parser<'i, 't>,
1553    options: &ParserOptions<'_, 'i>,
1554  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1555    let mut parser = ComponentParser::new(false);
1556    match_ignore_ascii_case! { &*f,
1557      "rgb" => {
1558        input.parse_nested_block(|input| {
1559          parser.parse_relative::<SRGB, _, _>(input, |input, parser| {
1560            let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
1561            if is_legacy {
1562              return Err(input.new_custom_error(ParserError::InvalidValue))
1563            }
1564            input.expect_delim('/')?;
1565            let alpha = TokenList::parse(input, options, 0)?;
1566            Ok(UnresolvedColor::RGB { r, g, b, alpha })
1567          })
1568        })
1569      },
1570      "hsl" => {
1571        input.parse_nested_block(|input| {
1572          parser.parse_relative::<HSL, _, _>(input, |input, parser| {
1573            let (h, s, l, is_legacy) = parse_hsl_hwb_components::<HSL>(input, parser, false)?;
1574            if is_legacy {
1575              return Err(input.new_custom_error(ParserError::InvalidValue))
1576            }
1577            input.expect_delim('/')?;
1578            let alpha = TokenList::parse(input, options, 0)?;
1579            Ok(UnresolvedColor::HSL { h, s, l, alpha })
1580          })
1581        })
1582      },
1583      "light-dark" => {
1584        input.parse_nested_block(|input| {
1585          let light = input.parse_until_before(Delimiter::Comma, |input|
1586            TokenList::parse(input, options, 0)
1587          )?;
1588          input.expect_comma()?;
1589          let dark = TokenList::parse(input, options, 0)?;
1590          Ok(UnresolvedColor::LightDark { light, dark })
1591        })
1592      },
1593      _ => Err(input.new_custom_error(ParserError::InvalidValue))
1594    }
1595  }
1596
1597  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1598  where
1599    W: std::fmt::Write,
1600  {
1601    #[inline]
1602    fn c(c: &f32) -> i32 {
1603      (c * 255.0).round().clamp(0.0, 255.0) as i32
1604    }
1605
1606    match self {
1607      UnresolvedColor::RGB { r, g, b, alpha } => {
1608        if should_compile!(dest.targets, SpaceSeparatedColorNotation) {
1609          dest.write_str("rgba(")?;
1610          c(r).to_css(dest)?;
1611          dest.delim(',', false)?;
1612          c(g).to_css(dest)?;
1613          dest.delim(',', false)?;
1614          c(b).to_css(dest)?;
1615          dest.delim(',', false)?;
1616          alpha.to_css(dest, is_custom_property)?;
1617          dest.write_char(')')?;
1618          return Ok(());
1619        }
1620
1621        dest.write_str("rgb(")?;
1622        c(r).to_css(dest)?;
1623        dest.write_char(' ')?;
1624        c(g).to_css(dest)?;
1625        dest.write_char(' ')?;
1626        c(b).to_css(dest)?;
1627        dest.delim('/', true)?;
1628        alpha.to_css(dest, is_custom_property)?;
1629        dest.write_char(')')
1630      }
1631      UnresolvedColor::HSL { h, s, l, alpha } => {
1632        if should_compile!(dest.targets, SpaceSeparatedColorNotation) {
1633          dest.write_str("hsla(")?;
1634          h.to_css(dest)?;
1635          dest.delim(',', false)?;
1636          Percentage(*s).to_css(dest)?;
1637          dest.delim(',', false)?;
1638          Percentage(*l).to_css(dest)?;
1639          dest.delim(',', false)?;
1640          alpha.to_css(dest, is_custom_property)?;
1641          dest.write_char(')')?;
1642          return Ok(());
1643        }
1644
1645        dest.write_str("hsl(")?;
1646        h.to_css(dest)?;
1647        dest.write_char(' ')?;
1648        Percentage(*s).to_css(dest)?;
1649        dest.write_char(' ')?;
1650        Percentage(*l).to_css(dest)?;
1651        dest.delim('/', true)?;
1652        alpha.to_css(dest, is_custom_property)?;
1653        dest.write_char(')')
1654      }
1655      UnresolvedColor::LightDark { light, dark } => {
1656        if !dest.targets.is_compatible(crate::compat::Feature::LightDark) {
1657          dest.write_str("var(--lightningcss-light")?;
1658          dest.delim(',', false)?;
1659          light.to_css(dest, is_custom_property)?;
1660          dest.write_char(')')?;
1661          dest.whitespace()?;
1662          dest.write_str("var(--lightningcss-dark")?;
1663          dest.delim(',', false)?;
1664          dark.to_css(dest, is_custom_property)?;
1665          return dest.write_char(')');
1666        }
1667
1668        dest.write_str("light-dark(")?;
1669        light.to_css(dest, is_custom_property)?;
1670        dest.delim(',', false)?;
1671        dark.to_css(dest, is_custom_property)?;
1672        dest.write_char(')')
1673      }
1674    }
1675  }
1676}