Skip to main content

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, Features, 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, RGB, RGBA,
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    loop {
374      let state = input.state();
375      match input.next_including_whitespace_and_comments() {
376        Ok(&cssparser::Token::Function(ref f)) => {
377          // Attempt to parse embedded color values into hex tokens.
378          let f = f.into();
379          if let Some(color) = try_parse_color_token(&f, &state, input) {
380            tokens.push(TokenOrValue::Color(color));
381          } else if let Ok(color) = input.try_parse(|input| UnresolvedColor::parse(&f, input, options)) {
382            tokens.push(TokenOrValue::UnresolvedColor(color));
383          } else if f == "url" {
384            input.reset(&state);
385            tokens.push(TokenOrValue::Url(Url::parse(input)?));
386          } else if f == "var" {
387            let var = input.parse_nested_block(|input| {
388              let var = Variable::parse(input, options, depth + 1)?;
389              Ok(TokenOrValue::Var(var))
390            })?;
391            tokens.push(var);
392          } else if f == "env" {
393            let env = input.parse_nested_block(|input| {
394              let env = EnvironmentVariable::parse_nested(input, options, depth + 1)?;
395              Ok(TokenOrValue::Env(env))
396            })?;
397            tokens.push(env);
398          } else {
399            let arguments = input.parse_nested_block(|input| TokenList::parse(input, options, depth + 1))?;
400            tokens.push(TokenOrValue::Function(Function {
401              name: Ident(f),
402              arguments,
403            }));
404          }
405        }
406        Ok(&cssparser::Token::Hash(ref h)) | Ok(&cssparser::Token::IDHash(ref h)) => {
407          if let Ok((r, g, b, a)) = parse_hash_color(h.as_bytes()) {
408            tokens.push(TokenOrValue::Color(CssColor::RGBA(RGBA::new(r, g, b, a))));
409          } else {
410            tokens.push(Token::Hash(h.into()).into());
411          }
412        }
413        Ok(&cssparser::Token::UnquotedUrl(_)) => {
414          input.reset(&state);
415          tokens.push(TokenOrValue::Url(Url::parse(input)?));
416        }
417        Ok(&cssparser::Token::Ident(ref name)) if name.starts_with("--") => {
418          tokens.push(TokenOrValue::DashedIdent(name.into()));
419        }
420        Ok(token @ &cssparser::Token::ParenthesisBlock)
421        | Ok(token @ &cssparser::Token::SquareBracketBlock)
422        | Ok(token @ &cssparser::Token::CurlyBracketBlock) => {
423          tokens.push(Token::from(token).into());
424          let closing_delimiter = match token {
425            cssparser::Token::ParenthesisBlock => Token::CloseParenthesis,
426            cssparser::Token::SquareBracketBlock => Token::CloseSquareBracket,
427            cssparser::Token::CurlyBracketBlock => Token::CloseCurlyBracket,
428            _ => unreachable!(),
429          };
430
431          input.parse_nested_block(|input| TokenList::parse_into(input, tokens, options, depth + 1))?;
432
433          tokens.push(closing_delimiter.into());
434        }
435        Ok(token @ cssparser::Token::Dimension { .. }) => {
436          let value = if let Ok(length) = LengthValue::try_from(token) {
437            TokenOrValue::Length(length)
438          } else if let Ok(angle) = Angle::try_from(token) {
439            TokenOrValue::Angle(angle)
440          } else if let Ok(time) = Time::try_from(token) {
441            TokenOrValue::Time(time)
442          } else if let Ok(resolution) = Resolution::try_from(token) {
443            TokenOrValue::Resolution(resolution)
444          } else {
445            TokenOrValue::Token(token.into())
446          };
447          tokens.push(value);
448        }
449        Ok(token) if token.is_parse_error() => {
450          return Err(ParseError {
451            kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token.clone())),
452            location: state.source_location(),
453          })
454        }
455        Ok(token) => {
456          tokens.push(Token::from(token).into());
457        }
458        Err(_) => break,
459      }
460    }
461
462    Ok(())
463  }
464}
465
466#[inline]
467fn try_parse_color_token<'i, 't>(
468  f: &CowArcStr<'i>,
469  state: &ParserState,
470  input: &mut Parser<'i, 't>,
471) -> Option<CssColor> {
472  match_ignore_ascii_case! { &*f,
473    "rgb" | "rgba" | "hsl" | "hsla" | "hwb" | "lab" | "lch" | "oklab" | "oklch" | "color" | "color-mix" | "light-dark" | "alpha" => {
474      let s = input.state();
475      input.reset(&state);
476      if let Ok(color) = CssColor::parse(input) {
477        return Some(color)
478      }
479      input.reset(&s);
480    },
481    _ => {}
482  }
483
484  None
485}
486
487impl<'i> TokenList<'i> {
488  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
489  where
490    W: std::fmt::Write,
491  {
492    for token_or_value in self.0.iter() {
493      match token_or_value {
494        TokenOrValue::Color(color) => {
495          color.to_css(dest)?;
496        }
497        TokenOrValue::UnresolvedColor(color) => {
498          color.to_css(dest, is_custom_property)?;
499        }
500        TokenOrValue::Url(url) => {
501          if dest.dependencies.is_some() && is_custom_property && !url.is_absolute() {
502            return Err(dest.error(
503              PrinterErrorKind::AmbiguousUrlInCustomProperty {
504                url: url.url.as_ref().to_owned(),
505              },
506              url.loc,
507            ));
508          }
509          url.to_css(dest)?;
510        }
511        TokenOrValue::Var(var) => {
512          var.to_css(dest, is_custom_property)?;
513        }
514        TokenOrValue::Env(env) => {
515          env.to_css(dest, is_custom_property)?;
516        }
517        TokenOrValue::Function(f) => {
518          f.to_css(dest, is_custom_property)?;
519        }
520        TokenOrValue::Length(v) => {
521          // Do not serialize unitless zero lengths in custom properties as it may break calc().
522          let (value, unit) = v.to_unit_value();
523          serialize_dimension(value, unit, dest)?;
524        }
525        TokenOrValue::Angle(v) => {
526          v.to_css(dest)?;
527        }
528        TokenOrValue::Time(v) => {
529          v.to_css(dest)?;
530        }
531        TokenOrValue::Resolution(v) => {
532          v.to_css(dest)?;
533        }
534        TokenOrValue::DashedIdent(v) => {
535          v.to_css(dest)?;
536        }
537        TokenOrValue::AnimationName(v) => {
538          v.to_css(dest)?;
539        }
540        TokenOrValue::Token(token) => match token {
541          Token::Dimension { value, unit, .. } => {
542            serialize_dimension(*value, unit, dest)?;
543          }
544          Token::Number { value, .. } => {
545            value.to_css(dest)?;
546          }
547          _ => {
548            token.to_css(dest)?;
549          }
550        },
551      };
552    }
553
554    Ok(())
555  }
556
557  pub(crate) fn to_css_raw<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
558  where
559    W: std::fmt::Write,
560  {
561    for token_or_value in &self.0 {
562      match token_or_value {
563        TokenOrValue::Token(token) => {
564          token.to_css(dest)?;
565        }
566        _ => {
567          return Err(PrinterError {
568            kind: PrinterErrorKind::FmtError,
569            loc: None,
570          })
571        }
572      }
573    }
574
575    Ok(())
576  }
577
578  pub(crate) fn starts_with_whitespace(&self) -> bool {
579    matches!(self.0.get(0), Some(TokenOrValue::Token(Token::WhiteSpace(_))))
580  }
581}
582
583/// A raw CSS token.
584// Copied from cssparser to change CowRcStr to CowArcStr
585#[derive(Debug, Clone, PartialEq)]
586#[cfg_attr(feature = "visitor", derive(Visit))]
587#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
588#[cfg_attr(
589  feature = "serde",
590  derive(serde::Serialize, serde::Deserialize),
591  serde(tag = "type", rename_all = "kebab-case")
592)]
593#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
594pub enum Token<'a> {
595  /// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram)
596  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
597  Ident(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'a>),
598
599  /// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
600  ///
601  /// The value does not include the `@` marker.
602  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
603  AtKeyword(CowArcStr<'a>),
604
605  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted"
606  ///
607  /// The value does not include the `#` marker.
608  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
609  Hash(CowArcStr<'a>),
610
611  /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id"
612  ///
613  /// The value does not include the `#` marker.
614  #[cfg_attr(feature = "serde", serde(rename = "id-hash", with = "ValueWrapper::<CowArcStr>"))]
615  IDHash(CowArcStr<'a>), // Hash that is a valid ID selector.
616
617  /// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram)
618  ///
619  /// The value does not include the quotes.
620  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
621  String(CowArcStr<'a>),
622
623  /// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram)
624  ///
625  /// The value does not include the `url(` `)` markers.  Note that `url( <string-token> )` is represented by a
626  /// `Function` token.
627  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
628  UnquotedUrl(CowArcStr<'a>),
629
630  /// A `<delim-token>`
631  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<char>"))]
632  Delim(char),
633
634  /// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram)
635  Number {
636    /// Whether the number had a `+` or `-` sign.
637    ///
638    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
639    #[cfg_attr(feature = "serde", serde(skip))]
640    has_sign: bool,
641
642    /// The value as a float
643    value: f32,
644
645    /// If the origin source did not include a fractional part, the value as an integer.
646    #[cfg_attr(feature = "serde", serde(skip))]
647    int_value: Option<i32>,
648  },
649
650  /// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
651  Percentage {
652    /// Whether the number had a `+` or `-` sign.
653    #[cfg_attr(feature = "serde", serde(skip))]
654    has_sign: bool,
655
656    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
657    #[cfg_attr(feature = "serde", serde(rename = "value"))]
658    unit_value: f32,
659
660    /// If the origin source did not include a fractional part, the value as an integer.
661    /// It is **not** divided by 100.
662    #[cfg_attr(feature = "serde", serde(skip))]
663    int_value: Option<i32>,
664  },
665
666  /// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
667  Dimension {
668    /// Whether the number had a `+` or `-` sign.
669    ///
670    /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
671    #[cfg_attr(feature = "serde", serde(skip))]
672    has_sign: bool,
673
674    /// The value as a float
675    value: f32,
676
677    /// If the origin source did not include a fractional part, the value as an integer.
678    #[cfg_attr(feature = "serde", serde(skip))]
679    int_value: Option<i32>,
680
681    /// The unit, e.g. "px" in `12px`
682    unit: CowArcStr<'a>,
683  },
684
685  /// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
686  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
687  WhiteSpace(CowArcStr<'a>),
688
689  /// A comment.
690  ///
691  /// The CSS Syntax spec does not generate tokens for comments,
692  /// But we do, because we can (borrowed &str makes it cheap).
693  ///
694  /// The value does not include the `/*` `*/` markers.
695  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
696  Comment(CowArcStr<'a>),
697
698  /// A `:` `<colon-token>`
699  Colon, // :
700
701  /// A `;` `<semicolon-token>`
702  Semicolon, // ;
703
704  /// A `,` `<comma-token>`
705  Comma, // ,
706
707  /// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram)
708  IncludeMatch,
709
710  /// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram)
711  DashMatch,
712
713  /// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram)
714  PrefixMatch,
715
716  /// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram)
717  SuffixMatch,
718
719  /// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
720  SubstringMatch,
721
722  /// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram)
723  #[cfg_attr(feature = "serde", serde(rename = "cdo"))]
724  CDO,
725
726  /// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram)
727  #[cfg_attr(feature = "serde", serde(rename = "cdc"))]
728  CDC,
729
730  /// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram)
731  ///
732  /// The value (name) does not include the `(` marker.
733  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
734  Function(CowArcStr<'a>),
735
736  /// A `<(-token>`
737  ParenthesisBlock,
738
739  /// A `<[-token>`
740  SquareBracketBlock,
741
742  /// A `<{-token>`
743  CurlyBracketBlock,
744
745  /// A `<bad-url-token>`
746  ///
747  /// This token always indicates a parse error.
748  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
749  BadUrl(CowArcStr<'a>),
750
751  /// A `<bad-string-token>`
752  ///
753  /// This token always indicates a parse error.
754  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<CowArcStr>"))]
755  BadString(CowArcStr<'a>),
756
757  /// A `<)-token>`
758  ///
759  /// When obtained from one of the `Parser::next*` methods,
760  /// this token is always unmatched and indicates a parse error.
761  CloseParenthesis,
762
763  /// A `<]-token>`
764  ///
765  /// When obtained from one of the `Parser::next*` methods,
766  /// this token is always unmatched and indicates a parse error.
767  CloseSquareBracket,
768
769  /// A `<}-token>`
770  ///
771  /// When obtained from one of the `Parser::next*` methods,
772  /// this token is always unmatched and indicates a parse error.
773  CloseCurlyBracket,
774}
775
776impl<'a> From<&cssparser::Token<'a>> for Token<'a> {
777  #[inline]
778  fn from(t: &cssparser::Token<'a>) -> Token<'a> {
779    match t {
780      cssparser::Token::Ident(x) => Token::Ident(x.into()),
781      cssparser::Token::AtKeyword(x) => Token::AtKeyword(x.into()),
782      cssparser::Token::Hash(x) => Token::Hash(x.into()),
783      cssparser::Token::IDHash(x) => Token::IDHash(x.into()),
784      cssparser::Token::QuotedString(x) => Token::String(x.into()),
785      cssparser::Token::UnquotedUrl(x) => Token::UnquotedUrl(x.into()),
786      cssparser::Token::Function(x) => Token::Function(x.into()),
787      cssparser::Token::BadUrl(x) => Token::BadUrl(x.into()),
788      cssparser::Token::BadString(x) => Token::BadString(x.into()),
789      cssparser::Token::Delim(c) => Token::Delim(*c),
790      cssparser::Token::Number {
791        has_sign,
792        value,
793        int_value,
794      } => Token::Number {
795        has_sign: *has_sign,
796        value: *value,
797        int_value: *int_value,
798      },
799      cssparser::Token::Dimension {
800        has_sign,
801        value,
802        int_value,
803        unit,
804      } => Token::Dimension {
805        has_sign: *has_sign,
806        value: *value,
807        int_value: *int_value,
808        unit: unit.into(),
809      },
810      cssparser::Token::Percentage {
811        has_sign,
812        unit_value,
813        int_value,
814      } => Token::Percentage {
815        has_sign: *has_sign,
816        unit_value: *unit_value,
817        int_value: *int_value,
818      },
819      cssparser::Token::WhiteSpace(w) => Token::WhiteSpace((*w).into()),
820      cssparser::Token::Comment(c) => Token::Comment((*c).into()),
821      cssparser::Token::Colon => Token::Colon,
822      cssparser::Token::Semicolon => Token::Semicolon,
823      cssparser::Token::Comma => Token::Comma,
824      cssparser::Token::IncludeMatch => Token::IncludeMatch,
825      cssparser::Token::DashMatch => Token::DashMatch,
826      cssparser::Token::PrefixMatch => Token::PrefixMatch,
827      cssparser::Token::SuffixMatch => Token::SuffixMatch,
828      cssparser::Token::SubstringMatch => Token::SubstringMatch,
829      cssparser::Token::CDO => Token::CDO,
830      cssparser::Token::CDC => Token::CDC,
831      cssparser::Token::ParenthesisBlock => Token::ParenthesisBlock,
832      cssparser::Token::SquareBracketBlock => Token::SquareBracketBlock,
833      cssparser::Token::CurlyBracketBlock => Token::CurlyBracketBlock,
834      cssparser::Token::CloseParenthesis => Token::CloseParenthesis,
835      cssparser::Token::CloseSquareBracket => Token::CloseSquareBracket,
836      cssparser::Token::CloseCurlyBracket => Token::CloseCurlyBracket,
837    }
838  }
839}
840
841impl<'a> ToCss for Token<'a> {
842  #[inline]
843  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
844  where
845    W: std::fmt::Write,
846  {
847    use cssparser::ToCss;
848    match self {
849      Token::Ident(x) => cssparser::Token::Ident(x.as_ref().into()).to_css(dest)?,
850      Token::AtKeyword(x) => cssparser::Token::AtKeyword(x.as_ref().into()).to_css(dest)?,
851      Token::Hash(x) => cssparser::Token::Hash(x.as_ref().into()).to_css(dest)?,
852      Token::IDHash(x) => cssparser::Token::IDHash(x.as_ref().into()).to_css(dest)?,
853      Token::String(x) => cssparser::Token::QuotedString(x.as_ref().into()).to_css(dest)?,
854      Token::UnquotedUrl(x) => cssparser::Token::UnquotedUrl(x.as_ref().into()).to_css(dest)?,
855      Token::Function(x) => cssparser::Token::Function(x.as_ref().into()).to_css(dest)?,
856      Token::BadUrl(x) => cssparser::Token::BadUrl(x.as_ref().into()).to_css(dest)?,
857      Token::BadString(x) => cssparser::Token::BadString(x.as_ref().into()).to_css(dest)?,
858      Token::Delim(c) => cssparser::Token::Delim(*c).to_css(dest)?,
859      Token::Number {
860        has_sign,
861        value,
862        int_value,
863      } => cssparser::Token::Number {
864        has_sign: *has_sign,
865        value: *value,
866        int_value: *int_value,
867      }
868      .to_css(dest)?,
869      Token::Dimension {
870        has_sign,
871        value,
872        int_value,
873        unit,
874      } => cssparser::Token::Dimension {
875        has_sign: *has_sign,
876        value: *value,
877        int_value: *int_value,
878        unit: unit.as_ref().into(),
879      }
880      .to_css(dest)?,
881      Token::Percentage {
882        has_sign,
883        unit_value,
884        int_value,
885      } => cssparser::Token::Percentage {
886        has_sign: *has_sign,
887        unit_value: *unit_value,
888        int_value: *int_value,
889      }
890      .to_css(dest)?,
891      Token::WhiteSpace(w) => {
892        if dest.minify {
893          dest.write_char(' ')?;
894        } else {
895          dest.write_str(&w)?;
896        }
897      }
898      Token::Comment(c) => {
899        if !dest.minify {
900          cssparser::Token::Comment(c).to_css(dest)?;
901        }
902      }
903      Token::Colon => cssparser::Token::Colon.to_css(dest)?,
904      Token::Semicolon => cssparser::Token::Semicolon.to_css(dest)?,
905      Token::Comma => cssparser::Token::Comma.to_css(dest)?,
906      Token::IncludeMatch => cssparser::Token::IncludeMatch.to_css(dest)?,
907      Token::DashMatch => cssparser::Token::DashMatch.to_css(dest)?,
908      Token::PrefixMatch => cssparser::Token::PrefixMatch.to_css(dest)?,
909      Token::SuffixMatch => cssparser::Token::SuffixMatch.to_css(dest)?,
910      Token::SubstringMatch => cssparser::Token::SubstringMatch.to_css(dest)?,
911      Token::CDO => cssparser::Token::CDO.to_css(dest)?,
912      Token::CDC => cssparser::Token::CDC.to_css(dest)?,
913      Token::ParenthesisBlock => cssparser::Token::ParenthesisBlock.to_css(dest)?,
914      Token::SquareBracketBlock => cssparser::Token::SquareBracketBlock.to_css(dest)?,
915      Token::CurlyBracketBlock => cssparser::Token::CurlyBracketBlock.to_css(dest)?,
916      Token::CloseParenthesis => cssparser::Token::CloseParenthesis.to_css(dest)?,
917      Token::CloseSquareBracket => cssparser::Token::CloseSquareBracket.to_css(dest)?,
918      Token::CloseCurlyBracket => cssparser::Token::CloseCurlyBracket.to_css(dest)?,
919    }
920
921    Ok(())
922  }
923}
924
925impl<'a> Eq for Token<'a> {}
926
927impl<'a> std::hash::Hash for Token<'a> {
928  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
929    let tag = std::mem::discriminant(self);
930    tag.hash(state);
931    match self {
932      Token::Ident(x) => x.hash(state),
933      Token::AtKeyword(x) => x.hash(state),
934      Token::Hash(x) => x.hash(state),
935      Token::IDHash(x) => x.hash(state),
936      Token::String(x) => x.hash(state),
937      Token::UnquotedUrl(x) => x.hash(state),
938      Token::Function(x) => x.hash(state),
939      Token::BadUrl(x) => x.hash(state),
940      Token::BadString(x) => x.hash(state),
941      Token::Delim(x) => x.hash(state),
942      Token::Number {
943        has_sign,
944        value,
945        int_value,
946      } => {
947        has_sign.hash(state);
948        integer_decode(*value).hash(state);
949        int_value.hash(state);
950      }
951      Token::Dimension {
952        has_sign,
953        value,
954        int_value,
955        unit,
956      } => {
957        has_sign.hash(state);
958        integer_decode(*value).hash(state);
959        int_value.hash(state);
960        unit.hash(state);
961      }
962      Token::Percentage {
963        has_sign,
964        unit_value,
965        int_value,
966      } => {
967        has_sign.hash(state);
968        integer_decode(*unit_value).hash(state);
969        int_value.hash(state);
970      }
971      Token::WhiteSpace(w) => w.hash(state),
972      Token::Comment(c) => c.hash(state),
973      Token::Colon
974      | Token::Semicolon
975      | Token::Comma
976      | Token::IncludeMatch
977      | Token::DashMatch
978      | Token::PrefixMatch
979      | Token::SuffixMatch
980      | Token::SubstringMatch
981      | Token::CDO
982      | Token::CDC
983      | Token::ParenthesisBlock
984      | Token::SquareBracketBlock
985      | Token::CurlyBracketBlock
986      | Token::CloseParenthesis
987      | Token::CloseSquareBracket
988      | Token::CloseCurlyBracket => {}
989    }
990  }
991}
992
993/// Converts a floating point value into its mantissa, exponent,
994/// and sign components so that it can be hashed.
995fn integer_decode(v: f32) -> (u32, i16, i8) {
996  let bits: u32 = f32::to_bits(v);
997  let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 };
998  let mut exponent: i16 = ((bits >> 23) & 0xff) as i16;
999  let mantissa = if exponent == 0 {
1000    (bits & 0x7fffff) << 1
1001  } else {
1002    (bits & 0x7fffff) | 0x800000
1003  };
1004  // Exponent bias + mantissa shift
1005  exponent -= 127 + 23;
1006  (mantissa, exponent, sign)
1007}
1008
1009impl<'i> TokenList<'i> {
1010  pub(crate) fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
1011    let mut fallbacks = ColorFallbackKind::empty();
1012    for token in &self.0 {
1013      match token {
1014        TokenOrValue::Color(color) => {
1015          fallbacks |= color.get_possible_fallbacks(targets);
1016        }
1017        TokenOrValue::Function(f) => {
1018          fallbacks |= f.arguments.get_necessary_fallbacks(targets);
1019        }
1020        TokenOrValue::Var(v) => {
1021          if let Some(fallback) = &v.fallback {
1022            fallbacks |= fallback.get_necessary_fallbacks(targets);
1023          }
1024        }
1025        TokenOrValue::Env(v) => {
1026          if let Some(fallback) = &v.fallback {
1027            fallbacks |= fallback.get_necessary_fallbacks(targets);
1028          }
1029        }
1030        _ => {}
1031      }
1032    }
1033
1034    fallbacks
1035  }
1036
1037  pub(crate) fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1038    let tokens = self
1039      .0
1040      .iter()
1041      .map(|token| match token {
1042        TokenOrValue::Color(color) => TokenOrValue::Color(color.get_fallback(kind)),
1043        TokenOrValue::Function(f) => TokenOrValue::Function(f.get_fallback(kind)),
1044        TokenOrValue::Var(v) => TokenOrValue::Var(v.get_fallback(kind)),
1045        TokenOrValue::Env(e) => TokenOrValue::Env(e.get_fallback(kind)),
1046        _ => token.clone(),
1047      })
1048      .collect();
1049    TokenList(tokens)
1050  }
1051
1052  pub(crate) fn get_fallbacks(&mut self, targets: Targets) -> Vec<(SupportsCondition<'i>, Self)> {
1053    // Get the full list of possible fallbacks, and remove the lowest one, which will replace
1054    // the original declaration. The remaining fallbacks need to be added as @supports rules.
1055    let mut fallbacks = self.get_necessary_fallbacks(targets);
1056    let lowest_fallback = fallbacks.lowest();
1057    fallbacks.remove(lowest_fallback);
1058
1059    let mut res = Vec::new();
1060    if fallbacks.contains(ColorFallbackKind::P3) {
1061      res.push((
1062        ColorFallbackKind::P3.supports_condition(),
1063        self.get_fallback(ColorFallbackKind::P3),
1064      ));
1065    }
1066
1067    if fallbacks.contains(ColorFallbackKind::LAB) {
1068      res.push((
1069        ColorFallbackKind::LAB.supports_condition(),
1070        self.get_fallback(ColorFallbackKind::LAB),
1071      ));
1072    }
1073
1074    if !lowest_fallback.is_empty() {
1075      for token in self.0.iter_mut() {
1076        match token {
1077          TokenOrValue::Color(color) => {
1078            *color = color.get_fallback(lowest_fallback);
1079          }
1080          TokenOrValue::Function(f) => *f = f.get_fallback(lowest_fallback),
1081          TokenOrValue::Var(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
1082          TokenOrValue::Env(v) if v.fallback.is_some() => *v = v.get_fallback(lowest_fallback),
1083          _ => {}
1084        }
1085      }
1086    }
1087
1088    res
1089  }
1090
1091  pub(crate) fn get_features(&self) -> Features {
1092    let mut features = Features::empty();
1093    for token in &self.0 {
1094      match token {
1095        TokenOrValue::Color(color) => {
1096          features |= color.get_features();
1097        }
1098        TokenOrValue::UnresolvedColor(unresolved_color) => {
1099          features |= Features::SpaceSeparatedColorNotation;
1100          match unresolved_color {
1101            UnresolvedColor::LightDark { light, dark } => {
1102              features |= Features::LightDark;
1103              features |= light.get_features();
1104              features |= dark.get_features();
1105            }
1106            _ => {}
1107          }
1108        }
1109        TokenOrValue::Function(f) => {
1110          features |= f.arguments.get_features();
1111        }
1112        TokenOrValue::Var(v) => {
1113          if let Some(fallback) = &v.fallback {
1114            features |= fallback.get_features();
1115          }
1116        }
1117        TokenOrValue::Env(v) => {
1118          if let Some(fallback) = &v.fallback {
1119            features |= fallback.get_features();
1120          }
1121        }
1122        _ => {}
1123      }
1124    }
1125
1126    features
1127  }
1128
1129  /// Substitutes variables with the provided values.
1130  #[cfg(feature = "substitute_variables")]
1131  #[cfg_attr(docsrs, doc(cfg(feature = "substitute_variables")))]
1132  pub fn substitute_variables(&mut self, vars: &std::collections::HashMap<&str, TokenList<'i>>) {
1133    self.visit(&mut VarInliner { vars }).unwrap()
1134  }
1135}
1136
1137#[cfg(feature = "substitute_variables")]
1138struct VarInliner<'a, 'i> {
1139  vars: &'a std::collections::HashMap<&'a str, TokenList<'i>>,
1140}
1141
1142#[cfg(feature = "substitute_variables")]
1143impl<'a, 'i> crate::visitor::Visitor<'i> for VarInliner<'a, 'i> {
1144  type Error = std::convert::Infallible;
1145
1146  fn visit_types(&self) -> crate::visitor::VisitTypes {
1147    crate::visit_types!(TOKENS | VARIABLES)
1148  }
1149
1150  fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) -> Result<(), Self::Error> {
1151    let mut i = 0;
1152    let mut seen = std::collections::HashSet::new();
1153    while i < tokens.0.len() {
1154      let token = &mut tokens.0[i];
1155      token.visit(self).unwrap();
1156      if let TokenOrValue::Var(var) = token {
1157        if let Some(value) = self.vars.get(var.name.ident.0.as_ref()) {
1158          // Ignore circular references.
1159          if seen.insert(var.name.ident.0.clone()) {
1160            tokens.0.splice(i..i + 1, value.0.iter().cloned());
1161            // Don't advance. We need to replace any variables in the value.
1162            continue;
1163          }
1164        } else if let Some(fallback) = &var.fallback {
1165          let fallback = fallback.0.clone();
1166          if seen.insert(var.name.ident.0.clone()) {
1167            tokens.0.splice(i..i + 1, fallback.into_iter());
1168            continue;
1169          }
1170        }
1171      }
1172      seen.clear();
1173      i += 1;
1174    }
1175    Ok(())
1176  }
1177}
1178
1179/// A CSS variable reference.
1180#[derive(Debug, Clone, PartialEq)]
1181#[cfg_attr(feature = "visitor", derive(Visit))]
1182#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1183#[cfg_attr(feature = "visitor", visit(visit_variable, VARIABLES))]
1184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1185#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1186pub struct Variable<'i> {
1187  /// The variable name.
1188  #[cfg_attr(feature = "serde", serde(borrow))]
1189  pub name: DashedIdentReference<'i>,
1190  /// A fallback value in case the variable is not defined.
1191  pub fallback: Option<TokenList<'i>>,
1192}
1193
1194impl<'i> Variable<'i> {
1195  fn parse<'t>(
1196    input: &mut Parser<'i, 't>,
1197    options: &ParserOptions<'i>,
1198    depth: usize,
1199  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1200    let name = DashedIdentReference::parse_with_options(input, options)?;
1201
1202    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
1203      Some(TokenList::parse(input, options, depth)?)
1204    } else {
1205      None
1206    };
1207
1208    Ok(Variable { name, fallback })
1209  }
1210
1211  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1212  where
1213    W: std::fmt::Write,
1214  {
1215    dest.write_str("var(")?;
1216    self.name.to_css(dest)?;
1217    if let Some(fallback) = &self.fallback {
1218      dest.write_char(',')?;
1219      if !fallback.starts_with_whitespace() {
1220        dest.whitespace()?;
1221      }
1222      fallback.to_css(dest, is_custom_property)?;
1223    }
1224    dest.write_char(')')
1225  }
1226
1227  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1228    Variable {
1229      name: self.name.clone(),
1230      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
1231    }
1232  }
1233}
1234
1235/// A CSS environment variable reference.
1236#[derive(Debug, Clone, PartialEq)]
1237#[cfg_attr(
1238  feature = "visitor",
1239  derive(Visit),
1240  visit(visit_environment_variable, ENVIRONMENT_VARIABLES)
1241)]
1242#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1244#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1245pub struct EnvironmentVariable<'i> {
1246  /// The environment variable name.
1247  #[cfg_attr(feature = "serde", serde(borrow))]
1248  pub name: EnvironmentVariableName<'i>,
1249  /// Optional indices into the dimensions of the environment variable.
1250  #[cfg_attr(feature = "serde", serde(default))]
1251  pub indices: Vec<CSSInteger>,
1252  /// A fallback value in case the variable is not defined.
1253  pub fallback: Option<TokenList<'i>>,
1254}
1255
1256/// A CSS environment variable name.
1257#[derive(Debug, Clone, PartialEq)]
1258#[cfg_attr(feature = "visitor", derive(Visit))]
1259#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1260#[cfg_attr(
1261  feature = "serde",
1262  derive(serde::Serialize, serde::Deserialize),
1263  serde(tag = "type", rename_all = "lowercase")
1264)]
1265#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1266pub enum EnvironmentVariableName<'i> {
1267  /// A UA-defined environment variable.
1268  #[cfg_attr(
1269    feature = "serde",
1270    serde(with = "crate::serialization::ValueWrapper::<UAEnvironmentVariable>")
1271  )]
1272  UA(UAEnvironmentVariable),
1273  /// A custom author-defined environment variable.
1274  #[cfg_attr(feature = "serde", serde(borrow))]
1275  Custom(DashedIdentReference<'i>),
1276  /// An unknown environment variable.
1277  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CustomIdent>"))]
1278  Unknown(CustomIdent<'i>),
1279}
1280
1281enum_property! {
1282  /// A UA-defined environment variable name.
1283  pub enum UAEnvironmentVariable {
1284    /// The safe area inset from the top of the viewport.
1285    SafeAreaInsetTop,
1286    /// The safe area inset from the right of the viewport.
1287    SafeAreaInsetRight,
1288    /// The safe area inset from the bottom of the viewport.
1289    SafeAreaInsetBottom,
1290    /// The safe area inset from the left of the viewport.
1291    SafeAreaInsetLeft,
1292    /// The viewport segment width.
1293    ViewportSegmentWidth,
1294    /// The viewport segment height.
1295    ViewportSegmentHeight,
1296    /// The viewport segment top position.
1297    ViewportSegmentTop,
1298    /// The viewport segment left position.
1299    ViewportSegmentLeft,
1300    /// The viewport segment bottom position.
1301    ViewportSegmentBottom,
1302    /// The viewport segment right position.
1303    ViewportSegmentRight,
1304  }
1305}
1306
1307impl<'i> EnvironmentVariableName<'i> {
1308  /// Returns the name of the environment variable as a string.
1309  pub fn name(&self) -> &str {
1310    match self {
1311      EnvironmentVariableName::UA(ua) => ua.as_str(),
1312      EnvironmentVariableName::Custom(c) => c.ident.as_ref(),
1313      EnvironmentVariableName::Unknown(u) => u.0.as_ref(),
1314    }
1315  }
1316}
1317
1318impl<'i> Parse<'i> for EnvironmentVariableName<'i> {
1319  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1320    if let Ok(ua) = input.try_parse(UAEnvironmentVariable::parse) {
1321      return Ok(EnvironmentVariableName::UA(ua));
1322    }
1323
1324    if let Ok(dashed) =
1325      input.try_parse(|input| DashedIdentReference::parse_with_options(input, &ParserOptions::default()))
1326    {
1327      return Ok(EnvironmentVariableName::Custom(dashed));
1328    }
1329
1330    let ident = CustomIdent::parse(input)?;
1331    return Ok(EnvironmentVariableName::Unknown(ident));
1332  }
1333}
1334
1335impl<'i> ToCss for EnvironmentVariableName<'i> {
1336  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1337  where
1338    W: std::fmt::Write,
1339  {
1340    match self {
1341      EnvironmentVariableName::UA(ua) => ua.to_css(dest),
1342      EnvironmentVariableName::Custom(custom) => custom.to_css(dest),
1343      EnvironmentVariableName::Unknown(unknown) => unknown.to_css(dest),
1344    }
1345  }
1346}
1347
1348impl<'i> EnvironmentVariable<'i> {
1349  pub(crate) fn parse<'t>(
1350    input: &mut Parser<'i, 't>,
1351    options: &ParserOptions<'i>,
1352    depth: usize,
1353  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1354    input.expect_function_matching("env")?;
1355    input.parse_nested_block(|input| Self::parse_nested(input, options, depth))
1356  }
1357
1358  pub(crate) fn parse_nested<'t>(
1359    input: &mut Parser<'i, 't>,
1360    options: &ParserOptions<'i>,
1361    depth: usize,
1362  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1363    let name = EnvironmentVariableName::parse(input)?;
1364    let mut indices = Vec::new();
1365    while let Ok(index) = input.try_parse(CSSInteger::parse) {
1366      indices.push(index);
1367    }
1368
1369    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
1370      Some(TokenList::parse(input, options, depth + 1)?)
1371    } else {
1372      None
1373    };
1374
1375    Ok(EnvironmentVariable {
1376      name,
1377      indices,
1378      fallback,
1379    })
1380  }
1381
1382  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1383  where
1384    W: std::fmt::Write,
1385  {
1386    dest.write_str("env(")?;
1387    self.name.to_css(dest)?;
1388
1389    for item in &self.indices {
1390      dest.write_char(' ')?;
1391      item.to_css(dest)?;
1392    }
1393
1394    if let Some(fallback) = &self.fallback {
1395      dest.write_char(',')?;
1396      if !fallback.starts_with_whitespace() {
1397        dest.whitespace()?;
1398      }
1399      fallback.to_css(dest, is_custom_property)?;
1400    }
1401    dest.write_char(')')
1402  }
1403
1404  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1405    EnvironmentVariable {
1406      name: self.name.clone(),
1407      indices: self.indices.clone(),
1408      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
1409    }
1410  }
1411}
1412
1413/// A custom CSS function.
1414#[derive(Debug, Clone, PartialEq)]
1415#[cfg_attr(feature = "visitor", derive(Visit))]
1416#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1417#[cfg_attr(feature = "visitor", visit(visit_function, FUNCTIONS))]
1418#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1419#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1420pub struct Function<'i> {
1421  /// The function name.
1422  #[cfg_attr(feature = "serde", serde(borrow))]
1423  pub name: Ident<'i>,
1424  /// The function arguments.
1425  pub arguments: TokenList<'i>,
1426}
1427
1428impl<'i> Function<'i> {
1429  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1430  where
1431    W: std::fmt::Write,
1432  {
1433    self.name.to_css(dest)?;
1434    dest.write_char('(')?;
1435    self.arguments.to_css(dest, is_custom_property)?;
1436    dest.write_char(')')
1437  }
1438
1439  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1440    Function {
1441      name: self.name.clone(),
1442      arguments: self.arguments.get_fallback(kind),
1443    }
1444  }
1445}
1446
1447/// A color value with an unresolved alpha value (e.g. a variable).
1448/// These can be converted from the modern slash syntax to older comma syntax.
1449/// This can only be done when the only unresolved component is the alpha
1450/// since variables can resolve to multiple tokens.
1451#[derive(Debug, Clone, PartialEq)]
1452#[cfg_attr(feature = "visitor", derive(Visit))]
1453#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1454#[cfg_attr(
1455  feature = "serde",
1456  derive(serde::Serialize, serde::Deserialize),
1457  serde(tag = "type", rename_all = "lowercase")
1458)]
1459#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1460pub enum UnresolvedColor<'i> {
1461  /// An rgb() color.
1462  RGB {
1463    /// The red component.
1464    r: f32,
1465    /// The green component.
1466    g: f32,
1467    /// The blue component.
1468    b: f32,
1469    /// The unresolved alpha component.
1470    #[cfg_attr(feature = "serde", serde(borrow))]
1471    alpha: TokenList<'i>,
1472  },
1473  /// An hsl() color.
1474  HSL {
1475    /// The hue component.
1476    h: f32,
1477    /// The saturation component.
1478    s: f32,
1479    /// The lightness component.
1480    l: f32,
1481    /// The unresolved alpha component.
1482    #[cfg_attr(feature = "serde", serde(borrow))]
1483    alpha: TokenList<'i>,
1484  },
1485  /// The light-dark() function.
1486  #[cfg_attr(feature = "serde", serde(rename = "light-dark"))]
1487  LightDark {
1488    /// The light value.
1489    light: TokenList<'i>,
1490    /// The dark value.
1491    dark: TokenList<'i>,
1492  },
1493}
1494
1495impl<'i> LightDarkColor for UnresolvedColor<'i> {
1496  #[inline]
1497  fn light_dark(light: Self, dark: Self) -> Self {
1498    UnresolvedColor::LightDark {
1499      light: TokenList(vec![TokenOrValue::UnresolvedColor(light)]),
1500      dark: TokenList(vec![TokenOrValue::UnresolvedColor(dark)]),
1501    }
1502  }
1503}
1504
1505impl<'i> UnresolvedColor<'i> {
1506  fn parse<'t>(
1507    f: &CowArcStr<'i>,
1508    input: &mut Parser<'i, 't>,
1509    options: &ParserOptions<'i>,
1510  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1511    let mut parser = ComponentParser::new(false);
1512    match_ignore_ascii_case! { &*f,
1513      "rgb" => {
1514        input.parse_nested_block(|input| {
1515          parser.parse_relative::<RGB, _, _>(input, |input, parser| {
1516            let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
1517            if is_legacy {
1518              return Err(input.new_custom_error(ParserError::InvalidValue))
1519            }
1520            input.expect_delim('/')?;
1521            let alpha = TokenList::parse(input, options, 0)?;
1522            Ok(UnresolvedColor::RGB { r, g, b, alpha })
1523          })
1524        })
1525      },
1526      "hsl" => {
1527        input.parse_nested_block(|input| {
1528          parser.parse_relative::<HSL, _, _>(input, |input, parser| {
1529            let (h, s, l, is_legacy) = parse_hsl_hwb_components::<HSL>(input, parser, false)?;
1530            if is_legacy {
1531              return Err(input.new_custom_error(ParserError::InvalidValue))
1532            }
1533            input.expect_delim('/')?;
1534            let alpha = TokenList::parse(input, options, 0)?;
1535            Ok(UnresolvedColor::HSL { h, s, l, alpha })
1536          })
1537        })
1538      },
1539      "light-dark" => {
1540        input.parse_nested_block(|input| {
1541          let light = input.parse_until_before(Delimiter::Comma, |input|
1542            TokenList::parse(input, options, 0)
1543          )?;
1544          input.expect_comma()?;
1545          let dark = TokenList::parse(input, options, 0)?;
1546          Ok(UnresolvedColor::LightDark { light, dark })
1547        })
1548      },
1549      _ => Err(input.new_custom_error(ParserError::InvalidValue))
1550    }
1551  }
1552
1553  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1554  where
1555    W: std::fmt::Write,
1556  {
1557    match self {
1558      UnresolvedColor::RGB { r, g, b, alpha } => {
1559        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
1560          dest.write_str("rgba(")?;
1561          r.to_css(dest)?;
1562          dest.delim(',', false)?;
1563          g.to_css(dest)?;
1564          dest.delim(',', false)?;
1565          b.to_css(dest)?;
1566          dest.delim(',', false)?;
1567          alpha.to_css(dest, is_custom_property)?;
1568          dest.write_char(')')?;
1569          return Ok(());
1570        }
1571
1572        dest.write_str("rgb(")?;
1573        r.to_css(dest)?;
1574        dest.write_char(' ')?;
1575        g.to_css(dest)?;
1576        dest.write_char(' ')?;
1577        b.to_css(dest)?;
1578        dest.delim('/', true)?;
1579        alpha.to_css(dest, is_custom_property)?;
1580        dest.write_char(')')
1581      }
1582      UnresolvedColor::HSL { h, s, l, alpha } => {
1583        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
1584          dest.write_str("hsla(")?;
1585          h.to_css(dest)?;
1586          dest.delim(',', false)?;
1587          Percentage(*s / 100.0).to_css(dest)?;
1588          dest.delim(',', false)?;
1589          Percentage(*l / 100.0).to_css(dest)?;
1590          dest.delim(',', false)?;
1591          alpha.to_css(dest, is_custom_property)?;
1592          dest.write_char(')')?;
1593          return Ok(());
1594        }
1595
1596        dest.write_str("hsl(")?;
1597        h.to_css(dest)?;
1598        dest.write_char(' ')?;
1599        Percentage(*s / 100.0).to_css(dest)?;
1600        dest.write_char(' ')?;
1601        Percentage(*l / 100.0).to_css(dest)?;
1602        dest.delim('/', true)?;
1603        alpha.to_css(dest, is_custom_property)?;
1604        dest.write_char(')')
1605      }
1606      UnresolvedColor::LightDark { light, dark } => {
1607        if should_compile!(dest.targets.current, LightDark) {
1608          dest.write_str("var(--lightningcss-light")?;
1609          dest.delim(',', false)?;
1610          light.to_css(dest, is_custom_property)?;
1611          dest.write_char(')')?;
1612          dest.whitespace()?;
1613          dest.write_str("var(--lightningcss-dark")?;
1614          dest.delim(',', false)?;
1615          dark.to_css(dest, is_custom_property)?;
1616          return dest.write_char(')');
1617        }
1618
1619        dest.write_str("light-dark(")?;
1620        light.to_css(dest, is_custom_property)?;
1621        dest.delim(',', false)?;
1622        dark.to_css(dest, is_custom_property)?;
1623        dest.write_char(')')
1624      }
1625    }
1626  }
1627}