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" => {
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 = unsafe { std::mem::transmute(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.delim(',', false)?;
1219      fallback.to_css(dest, is_custom_property)?;
1220    }
1221    dest.write_char(')')
1222  }
1223
1224  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1225    Variable {
1226      name: self.name.clone(),
1227      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
1228    }
1229  }
1230}
1231
1232/// A CSS environment variable reference.
1233#[derive(Debug, Clone, PartialEq)]
1234#[cfg_attr(
1235  feature = "visitor",
1236  derive(Visit),
1237  visit(visit_environment_variable, ENVIRONMENT_VARIABLES)
1238)]
1239#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1240#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1241#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1242pub struct EnvironmentVariable<'i> {
1243  /// The environment variable name.
1244  #[cfg_attr(feature = "serde", serde(borrow))]
1245  pub name: EnvironmentVariableName<'i>,
1246  /// Optional indices into the dimensions of the environment variable.
1247  #[cfg_attr(feature = "serde", serde(default))]
1248  pub indices: Vec<CSSInteger>,
1249  /// A fallback value in case the variable is not defined.
1250  pub fallback: Option<TokenList<'i>>,
1251}
1252
1253/// A CSS environment variable name.
1254#[derive(Debug, Clone, PartialEq)]
1255#[cfg_attr(feature = "visitor", derive(Visit))]
1256#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1257#[cfg_attr(
1258  feature = "serde",
1259  derive(serde::Serialize, serde::Deserialize),
1260  serde(tag = "type", rename_all = "lowercase")
1261)]
1262#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1263pub enum EnvironmentVariableName<'i> {
1264  /// A UA-defined environment variable.
1265  #[cfg_attr(
1266    feature = "serde",
1267    serde(with = "crate::serialization::ValueWrapper::<UAEnvironmentVariable>")
1268  )]
1269  UA(UAEnvironmentVariable),
1270  /// A custom author-defined environment variable.
1271  #[cfg_attr(feature = "serde", serde(borrow))]
1272  Custom(DashedIdentReference<'i>),
1273  /// An unknown environment variable.
1274  #[cfg_attr(feature = "serde", serde(with = "crate::serialization::ValueWrapper::<CustomIdent>"))]
1275  Unknown(CustomIdent<'i>),
1276}
1277
1278enum_property! {
1279  /// A UA-defined environment variable name.
1280  pub enum UAEnvironmentVariable {
1281    /// The safe area inset from the top of the viewport.
1282    SafeAreaInsetTop,
1283    /// The safe area inset from the right of the viewport.
1284    SafeAreaInsetRight,
1285    /// The safe area inset from the bottom of the viewport.
1286    SafeAreaInsetBottom,
1287    /// The safe area inset from the left of the viewport.
1288    SafeAreaInsetLeft,
1289    /// The viewport segment width.
1290    ViewportSegmentWidth,
1291    /// The viewport segment height.
1292    ViewportSegmentHeight,
1293    /// The viewport segment top position.
1294    ViewportSegmentTop,
1295    /// The viewport segment left position.
1296    ViewportSegmentLeft,
1297    /// The viewport segment bottom position.
1298    ViewportSegmentBottom,
1299    /// The viewport segment right position.
1300    ViewportSegmentRight,
1301  }
1302}
1303
1304impl<'i> EnvironmentVariableName<'i> {
1305  /// Returns the name of the environment variable as a string.
1306  pub fn name(&self) -> &str {
1307    match self {
1308      EnvironmentVariableName::UA(ua) => ua.as_str(),
1309      EnvironmentVariableName::Custom(c) => c.ident.as_ref(),
1310      EnvironmentVariableName::Unknown(u) => u.0.as_ref(),
1311    }
1312  }
1313}
1314
1315impl<'i> Parse<'i> for EnvironmentVariableName<'i> {
1316  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1317    if let Ok(ua) = input.try_parse(UAEnvironmentVariable::parse) {
1318      return Ok(EnvironmentVariableName::UA(ua));
1319    }
1320
1321    if let Ok(dashed) =
1322      input.try_parse(|input| DashedIdentReference::parse_with_options(input, &ParserOptions::default()))
1323    {
1324      return Ok(EnvironmentVariableName::Custom(dashed));
1325    }
1326
1327    let ident = CustomIdent::parse(input)?;
1328    return Ok(EnvironmentVariableName::Unknown(ident));
1329  }
1330}
1331
1332impl<'i> ToCss for EnvironmentVariableName<'i> {
1333  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1334  where
1335    W: std::fmt::Write,
1336  {
1337    match self {
1338      EnvironmentVariableName::UA(ua) => ua.to_css(dest),
1339      EnvironmentVariableName::Custom(custom) => custom.to_css(dest),
1340      EnvironmentVariableName::Unknown(unknown) => unknown.to_css(dest),
1341    }
1342  }
1343}
1344
1345impl<'i> EnvironmentVariable<'i> {
1346  pub(crate) fn parse<'t>(
1347    input: &mut Parser<'i, 't>,
1348    options: &ParserOptions<'_, 'i>,
1349    depth: usize,
1350  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1351    input.expect_function_matching("env")?;
1352    input.parse_nested_block(|input| Self::parse_nested(input, options, depth))
1353  }
1354
1355  pub(crate) fn parse_nested<'t>(
1356    input: &mut Parser<'i, 't>,
1357    options: &ParserOptions<'_, 'i>,
1358    depth: usize,
1359  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1360    let name = EnvironmentVariableName::parse(input)?;
1361    let mut indices = Vec::new();
1362    while let Ok(index) = input.try_parse(CSSInteger::parse) {
1363      indices.push(index);
1364    }
1365
1366    let fallback = if input.try_parse(|input| input.expect_comma()).is_ok() {
1367      Some(TokenList::parse(input, options, depth + 1)?)
1368    } else {
1369      None
1370    };
1371
1372    Ok(EnvironmentVariable {
1373      name,
1374      indices,
1375      fallback,
1376    })
1377  }
1378
1379  pub(crate) fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1380  where
1381    W: std::fmt::Write,
1382  {
1383    dest.write_str("env(")?;
1384    self.name.to_css(dest)?;
1385
1386    for item in &self.indices {
1387      dest.write_char(' ')?;
1388      item.to_css(dest)?;
1389    }
1390
1391    if let Some(fallback) = &self.fallback {
1392      dest.delim(',', false)?;
1393      fallback.to_css(dest, is_custom_property)?;
1394    }
1395    dest.write_char(')')
1396  }
1397
1398  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1399    EnvironmentVariable {
1400      name: self.name.clone(),
1401      indices: self.indices.clone(),
1402      fallback: self.fallback.as_ref().map(|fallback| fallback.get_fallback(kind)),
1403    }
1404  }
1405}
1406
1407/// A custom CSS function.
1408#[derive(Debug, Clone, PartialEq)]
1409#[cfg_attr(feature = "visitor", derive(Visit))]
1410#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1411#[cfg_attr(feature = "visitor", visit(visit_function, FUNCTIONS))]
1412#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1413#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1414pub struct Function<'i> {
1415  /// The function name.
1416  #[cfg_attr(feature = "serde", serde(borrow))]
1417  pub name: Ident<'i>,
1418  /// The function arguments.
1419  pub arguments: TokenList<'i>,
1420}
1421
1422impl<'i> Function<'i> {
1423  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1424  where
1425    W: std::fmt::Write,
1426  {
1427    self.name.to_css(dest)?;
1428    dest.write_char('(')?;
1429    self.arguments.to_css(dest, is_custom_property)?;
1430    dest.write_char(')')
1431  }
1432
1433  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
1434    Function {
1435      name: self.name.clone(),
1436      arguments: self.arguments.get_fallback(kind),
1437    }
1438  }
1439}
1440
1441/// A color value with an unresolved alpha value (e.g. a variable).
1442/// These can be converted from the modern slash syntax to older comma syntax.
1443/// This can only be done when the only unresolved component is the alpha
1444/// since variables can resolve to multiple tokens.
1445#[derive(Debug, Clone, PartialEq)]
1446#[cfg_attr(feature = "visitor", derive(Visit))]
1447#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1448#[cfg_attr(
1449  feature = "serde",
1450  derive(serde::Serialize, serde::Deserialize),
1451  serde(tag = "type", rename_all = "lowercase")
1452)]
1453#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1454pub enum UnresolvedColor<'i> {
1455  /// An rgb() color.
1456  RGB {
1457    /// The red component.
1458    r: f32,
1459    /// The green component.
1460    g: f32,
1461    /// The blue component.
1462    b: f32,
1463    /// The unresolved alpha component.
1464    #[cfg_attr(feature = "serde", serde(borrow))]
1465    alpha: TokenList<'i>,
1466  },
1467  /// An hsl() color.
1468  HSL {
1469    /// The hue component.
1470    h: f32,
1471    /// The saturation component.
1472    s: f32,
1473    /// The lightness component.
1474    l: f32,
1475    /// The unresolved alpha component.
1476    #[cfg_attr(feature = "serde", serde(borrow))]
1477    alpha: TokenList<'i>,
1478  },
1479  /// The light-dark() function.
1480  #[cfg_attr(feature = "serde", serde(rename = "light-dark"))]
1481  LightDark {
1482    /// The light value.
1483    light: TokenList<'i>,
1484    /// The dark value.
1485    dark: TokenList<'i>,
1486  },
1487}
1488
1489impl<'i> LightDarkColor for UnresolvedColor<'i> {
1490  #[inline]
1491  fn light_dark(light: Self, dark: Self) -> Self {
1492    UnresolvedColor::LightDark {
1493      light: TokenList(vec![TokenOrValue::UnresolvedColor(light)]),
1494      dark: TokenList(vec![TokenOrValue::UnresolvedColor(dark)]),
1495    }
1496  }
1497}
1498
1499impl<'i> UnresolvedColor<'i> {
1500  fn parse<'t>(
1501    f: &CowArcStr<'i>,
1502    input: &mut Parser<'i, 't>,
1503    options: &ParserOptions<'_, 'i>,
1504  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1505    let mut parser = ComponentParser::new(false);
1506    match_ignore_ascii_case! { &*f,
1507      "rgb" => {
1508        input.parse_nested_block(|input| {
1509          parser.parse_relative::<RGB, _, _>(input, |input, parser| {
1510            let (r, g, b, is_legacy) = parse_rgb_components(input, parser)?;
1511            if is_legacy {
1512              return Err(input.new_custom_error(ParserError::InvalidValue))
1513            }
1514            input.expect_delim('/')?;
1515            let alpha = TokenList::parse(input, options, 0)?;
1516            Ok(UnresolvedColor::RGB { r, g, b, alpha })
1517          })
1518        })
1519      },
1520      "hsl" => {
1521        input.parse_nested_block(|input| {
1522          parser.parse_relative::<HSL, _, _>(input, |input, parser| {
1523            let (h, s, l, is_legacy) = parse_hsl_hwb_components::<HSL>(input, parser, false)?;
1524            if is_legacy {
1525              return Err(input.new_custom_error(ParserError::InvalidValue))
1526            }
1527            input.expect_delim('/')?;
1528            let alpha = TokenList::parse(input, options, 0)?;
1529            Ok(UnresolvedColor::HSL { h, s, l, alpha })
1530          })
1531        })
1532      },
1533      "light-dark" => {
1534        input.parse_nested_block(|input| {
1535          let light = input.parse_until_before(Delimiter::Comma, |input|
1536            TokenList::parse(input, options, 0)
1537          )?;
1538          input.expect_comma()?;
1539          let dark = TokenList::parse(input, options, 0)?;
1540          Ok(UnresolvedColor::LightDark { light, dark })
1541        })
1542      },
1543      _ => Err(input.new_custom_error(ParserError::InvalidValue))
1544    }
1545  }
1546
1547  fn to_css<W>(&self, dest: &mut Printer<W>, is_custom_property: bool) -> Result<(), PrinterError>
1548  where
1549    W: std::fmt::Write,
1550  {
1551    match self {
1552      UnresolvedColor::RGB { r, g, b, alpha } => {
1553        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
1554          dest.write_str("rgba(")?;
1555          r.to_css(dest)?;
1556          dest.delim(',', false)?;
1557          g.to_css(dest)?;
1558          dest.delim(',', false)?;
1559          b.to_css(dest)?;
1560          dest.delim(',', false)?;
1561          alpha.to_css(dest, is_custom_property)?;
1562          dest.write_char(')')?;
1563          return Ok(());
1564        }
1565
1566        dest.write_str("rgb(")?;
1567        r.to_css(dest)?;
1568        dest.write_char(' ')?;
1569        g.to_css(dest)?;
1570        dest.write_char(' ')?;
1571        b.to_css(dest)?;
1572        dest.delim('/', true)?;
1573        alpha.to_css(dest, is_custom_property)?;
1574        dest.write_char(')')
1575      }
1576      UnresolvedColor::HSL { h, s, l, alpha } => {
1577        if should_compile!(dest.targets.current, SpaceSeparatedColorNotation) {
1578          dest.write_str("hsla(")?;
1579          h.to_css(dest)?;
1580          dest.delim(',', false)?;
1581          Percentage(*s / 100.0).to_css(dest)?;
1582          dest.delim(',', false)?;
1583          Percentage(*l / 100.0).to_css(dest)?;
1584          dest.delim(',', false)?;
1585          alpha.to_css(dest, is_custom_property)?;
1586          dest.write_char(')')?;
1587          return Ok(());
1588        }
1589
1590        dest.write_str("hsl(")?;
1591        h.to_css(dest)?;
1592        dest.write_char(' ')?;
1593        Percentage(*s / 100.0).to_css(dest)?;
1594        dest.write_char(' ')?;
1595        Percentage(*l / 100.0).to_css(dest)?;
1596        dest.delim('/', true)?;
1597        alpha.to_css(dest, is_custom_property)?;
1598        dest.write_char(')')
1599      }
1600      UnresolvedColor::LightDark { light, dark } => {
1601        if should_compile!(dest.targets.current, LightDark) {
1602          dest.write_str("var(--lightningcss-light")?;
1603          dest.delim(',', false)?;
1604          light.to_css(dest, is_custom_property)?;
1605          dest.write_char(')')?;
1606          dest.whitespace()?;
1607          dest.write_str("var(--lightningcss-dark")?;
1608          dest.delim(',', false)?;
1609          dark.to_css(dest, is_custom_property)?;
1610          return dest.write_char(')');
1611        }
1612
1613        dest.write_str("light-dark(")?;
1614        light.to_css(dest, is_custom_property)?;
1615        dest.delim(',', false)?;
1616        dark.to_css(dest, is_custom_property)?;
1617        dest.write_char(')')
1618      }
1619    }
1620  }
1621}