lightningcss/rules/
font_palette_values.rs

1//! The `@font-palette-values` rule.
2
3use super::supports::SupportsRule;
4use super::{CssRule, CssRuleList, Location, MinifyContext};
5use crate::error::{ParserError, PrinterError};
6use crate::printer::Printer;
7use crate::properties::custom::CustomProperty;
8use crate::properties::font::FontFamily;
9use crate::stylesheet::ParserOptions;
10use crate::targets::Targets;
11use crate::traits::{Parse, ToCss};
12use crate::values::color::{ColorFallbackKind, CssColor};
13use crate::values::ident::DashedIdent;
14use crate::values::number::CSSInteger;
15#[cfg(feature = "visitor")]
16use crate::visitor::Visit;
17use cssparser::*;
18
19/// A [@font-palette-values](https://drafts.csswg.org/css-fonts-4/#font-palette-values) rule.
20#[derive(Debug, PartialEq, Clone)]
21#[cfg_attr(feature = "visitor", derive(Visit))]
22#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
25pub struct FontPaletteValuesRule<'i> {
26  /// The name of the font palette.
27  pub name: DashedIdent<'i>,
28  /// Declarations in the `@font-palette-values` rule.
29  #[cfg_attr(feature = "serde", serde(borrow))]
30  pub properties: Vec<FontPaletteValuesProperty<'i>>,
31  /// The location of the rule in the source file.
32  #[cfg_attr(feature = "visitor", skip_visit)]
33  pub loc: Location,
34}
35
36/// A property within an `@font-palette-values` rule.
37///
38///  See [FontPaletteValuesRule](FontPaletteValuesRule).
39#[derive(Debug, Clone, PartialEq)]
40#[cfg_attr(feature = "visitor", derive(Visit))]
41#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
42#[cfg_attr(
43  feature = "serde",
44  derive(serde::Serialize, serde::Deserialize),
45  serde(tag = "type", content = "value", rename_all = "kebab-case")
46)]
47#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
48pub enum FontPaletteValuesProperty<'i> {
49  /// The `font-family` property.
50  #[cfg_attr(feature = "serde", serde(borrow))]
51  FontFamily(FontFamily<'i>),
52  /// The `base-palette` property.
53  BasePalette(BasePalette),
54  /// The `override-colors` property.
55  OverrideColors(Vec<OverrideColors>),
56  /// An unknown or unsupported property.
57  Custom(CustomProperty<'i>),
58}
59
60/// A value for the [base-palette](https://drafts.csswg.org/css-fonts-4/#base-palette-desc)
61/// property in an `@font-palette-values` rule.
62#[derive(Debug, PartialEq, Clone)]
63#[cfg_attr(feature = "visitor", derive(Visit))]
64#[cfg_attr(
65  feature = "serde",
66  derive(serde::Serialize, serde::Deserialize),
67  serde(tag = "type", content = "value", rename_all = "kebab-case")
68)]
69#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
70#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
71pub enum BasePalette {
72  /// A light color palette as defined within the font.
73  Light,
74  /// A dark color palette as defined within the font.
75  Dark,
76  /// A palette index within the font.
77  Integer(u16),
78}
79
80/// A value for the [override-colors](https://drafts.csswg.org/css-fonts-4/#override-color)
81/// property in an `@font-palette-values` rule.
82#[derive(Debug, PartialEq, Clone)]
83#[cfg_attr(feature = "visitor", derive(Visit))]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
86#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
87pub struct OverrideColors {
88  /// The index of the color within the palette to override.
89  index: u16,
90  /// The replacement color.
91  color: CssColor,
92}
93
94pub(crate) struct FontPaletteValuesDeclarationParser;
95
96impl<'i> cssparser::DeclarationParser<'i> for FontPaletteValuesDeclarationParser {
97  type Declaration = FontPaletteValuesProperty<'i>;
98  type Error = ParserError<'i>;
99
100  fn parse_value<'t>(
101    &mut self,
102    name: CowRcStr<'i>,
103    input: &mut cssparser::Parser<'i, 't>,
104  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
105    let state = input.state();
106    match_ignore_ascii_case! { &name,
107      "font-family" => {
108        // https://drafts.csswg.org/css-fonts-4/#font-family-2-desc
109        if let Ok(font_family) = FontFamily::parse(input) {
110          return match font_family {
111            FontFamily::Generic(_) => Err(input.new_custom_error(ParserError::InvalidDeclaration)),
112            _ => Ok(FontPaletteValuesProperty::FontFamily(font_family))
113          }
114        }
115      },
116      "base-palette" => {
117        // https://drafts.csswg.org/css-fonts-4/#base-palette-desc
118        if let Ok(base_palette) = BasePalette::parse(input) {
119          return Ok(FontPaletteValuesProperty::BasePalette(base_palette))
120        }
121      },
122      "override-colors" => {
123        // https://drafts.csswg.org/css-fonts-4/#override-color
124        if let Ok(override_colors) = input.parse_comma_separated(OverrideColors::parse) {
125          return Ok(FontPaletteValuesProperty::OverrideColors(override_colors))
126        }
127      },
128      _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration))
129    }
130
131    input.reset(&state);
132    return Ok(FontPaletteValuesProperty::Custom(CustomProperty::parse(
133      name.into(),
134      input,
135      &ParserOptions::default(),
136    )?));
137  }
138}
139
140/// Default methods reject all at rules.
141impl<'i> AtRuleParser<'i> for FontPaletteValuesDeclarationParser {
142  type Prelude = ();
143  type AtRule = FontPaletteValuesProperty<'i>;
144  type Error = ParserError<'i>;
145}
146
147impl<'i> QualifiedRuleParser<'i> for FontPaletteValuesDeclarationParser {
148  type Prelude = ();
149  type QualifiedRule = FontPaletteValuesProperty<'i>;
150  type Error = ParserError<'i>;
151}
152
153impl<'i> RuleBodyItemParser<'i, FontPaletteValuesProperty<'i>, ParserError<'i>>
154  for FontPaletteValuesDeclarationParser
155{
156  fn parse_qualified(&self) -> bool {
157    false
158  }
159
160  fn parse_declarations(&self) -> bool {
161    true
162  }
163}
164
165impl<'i> FontPaletteValuesRule<'i> {
166  pub(crate) fn parse<'t>(
167    name: DashedIdent<'i>,
168    input: &mut Parser<'i, 't>,
169    loc: Location,
170  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
171    let mut decl_parser = FontPaletteValuesDeclarationParser;
172    let mut parser = RuleBodyParser::new(input, &mut decl_parser);
173    let mut properties = vec![];
174    while let Some(decl) = parser.next() {
175      if let Ok(decl) = decl {
176        properties.push(decl);
177      }
178    }
179
180    Ok(FontPaletteValuesRule { name, properties, loc })
181  }
182}
183
184impl<'i> Parse<'i> for BasePalette {
185  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
186    if let Ok(i) = input.try_parse(CSSInteger::parse) {
187      if i.is_negative() {
188        return Err(input.new_custom_error(ParserError::InvalidValue));
189      }
190      return Ok(BasePalette::Integer(i as u16));
191    }
192
193    let location = input.current_source_location();
194    let ident = input.expect_ident()?;
195    match_ignore_ascii_case! { &*ident,
196      "light" => Ok(BasePalette::Light),
197      "dark" => Ok(BasePalette::Dark),
198      _ => Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
199    }
200  }
201}
202
203impl ToCss for BasePalette {
204  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
205  where
206    W: std::fmt::Write,
207  {
208    match self {
209      BasePalette::Light => dest.write_str("light"),
210      BasePalette::Dark => dest.write_str("dark"),
211      BasePalette::Integer(i) => (*i as CSSInteger).to_css(dest),
212    }
213  }
214}
215
216impl<'i> Parse<'i> for OverrideColors {
217  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
218    let index = CSSInteger::parse(input)?;
219    if index.is_negative() {
220      return Err(input.new_custom_error(ParserError::InvalidValue));
221    }
222
223    let color = CssColor::parse(input)?;
224    if matches!(color, CssColor::CurrentColor) {
225      return Err(input.new_custom_error(ParserError::InvalidValue));
226    }
227
228    Ok(OverrideColors {
229      index: index as u16,
230      color,
231    })
232  }
233}
234
235impl ToCss for OverrideColors {
236  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
237  where
238    W: std::fmt::Write,
239  {
240    (self.index as CSSInteger).to_css(dest)?;
241    dest.write_char(' ')?;
242    self.color.to_css(dest)
243  }
244}
245
246impl OverrideColors {
247  fn get_fallback(&self, kind: ColorFallbackKind) -> OverrideColors {
248    OverrideColors {
249      index: self.index,
250      color: self.color.get_fallback(kind),
251    }
252  }
253}
254
255impl<'i> FontPaletteValuesRule<'i> {
256  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, _: bool) {
257    let mut properties = Vec::with_capacity(self.properties.len());
258    for property in &self.properties {
259      match property {
260        FontPaletteValuesProperty::OverrideColors(override_colors) => {
261          // Generate color fallbacks.
262          let mut fallbacks = ColorFallbackKind::empty();
263          for o in override_colors {
264            fallbacks |= o.color.get_necessary_fallbacks(*context.targets);
265          }
266
267          if fallbacks.contains(ColorFallbackKind::RGB) {
268            properties.push(FontPaletteValuesProperty::OverrideColors(
269              override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::RGB)).collect(),
270            ));
271          }
272
273          if fallbacks.contains(ColorFallbackKind::P3) {
274            properties.push(FontPaletteValuesProperty::OverrideColors(
275              override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::P3)).collect(),
276            ));
277          }
278
279          let override_colors = if fallbacks.contains(ColorFallbackKind::LAB) {
280            override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::P3)).collect()
281          } else {
282            override_colors.clone()
283          };
284
285          properties.push(FontPaletteValuesProperty::OverrideColors(override_colors));
286        }
287        _ => properties.push(property.clone()),
288      }
289    }
290
291    self.properties = properties;
292  }
293
294  pub(crate) fn get_fallbacks<T>(&mut self, targets: Targets) -> Vec<CssRule<'i, T>> {
295    // Get fallbacks for unparsed properties. These will generate @supports rules
296    // containing duplicate @font-palette-values rules.
297    let mut fallbacks = ColorFallbackKind::empty();
298    for property in &self.properties {
299      match property {
300        FontPaletteValuesProperty::Custom(CustomProperty { value, .. }) => {
301          fallbacks |= value.get_necessary_fallbacks(targets);
302        }
303        _ => {}
304      }
305    }
306
307    let mut res = Vec::new();
308    let lowest_fallback = fallbacks.lowest();
309    fallbacks.remove(lowest_fallback);
310
311    if fallbacks.contains(ColorFallbackKind::P3) {
312      res.push(self.get_fallback(ColorFallbackKind::P3));
313    }
314
315    if fallbacks.contains(ColorFallbackKind::LAB)
316      || (!lowest_fallback.is_empty() && lowest_fallback != ColorFallbackKind::LAB)
317    {
318      res.push(self.get_fallback(ColorFallbackKind::LAB));
319    }
320
321    if !lowest_fallback.is_empty() {
322      for property in &mut self.properties {
323        match property {
324          FontPaletteValuesProperty::Custom(CustomProperty { value, .. }) => {
325            *value = value.get_fallback(lowest_fallback);
326          }
327          _ => {}
328        }
329      }
330    }
331
332    res
333  }
334
335  fn get_fallback<T>(&self, kind: ColorFallbackKind) -> CssRule<'i, T> {
336    let properties = self
337      .properties
338      .iter()
339      .map(|property| match property {
340        FontPaletteValuesProperty::Custom(custom) => FontPaletteValuesProperty::Custom(CustomProperty {
341          name: custom.name.clone(),
342          value: custom.value.get_fallback(kind),
343        }),
344        _ => property.clone(),
345      })
346      .collect();
347    CssRule::Supports(SupportsRule {
348      condition: kind.supports_condition(),
349      rules: CssRuleList(vec![CssRule::FontPaletteValues(FontPaletteValuesRule {
350        name: self.name.clone(),
351        properties,
352        loc: self.loc.clone(),
353      })]),
354      loc: self.loc.clone(),
355    })
356  }
357}
358
359impl<'i> ToCss for FontPaletteValuesRule<'i> {
360  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
361  where
362    W: std::fmt::Write,
363  {
364    #[cfg(feature = "sourcemap")]
365    dest.add_mapping(self.loc);
366    dest.write_str("@font-palette-values ")?;
367    self.name.to_css(dest)?;
368    dest.whitespace()?;
369    dest.write_char('{')?;
370    dest.indent();
371    let len = self.properties.len();
372    for (i, prop) in self.properties.iter().enumerate() {
373      dest.newline()?;
374      prop.to_css(dest)?;
375      if i != len - 1 || !dest.minify {
376        dest.write_char(';')?;
377      }
378    }
379    dest.dedent();
380    dest.newline()?;
381    dest.write_char('}')
382  }
383}
384
385impl<'i> ToCss for FontPaletteValuesProperty<'i> {
386  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
387  where
388    W: std::fmt::Write,
389  {
390    macro_rules! property {
391      ($prop: literal, $value: expr) => {{
392        dest.write_str($prop)?;
393        dest.delim(':', false)?;
394        $value.to_css(dest)
395      }};
396    }
397
398    match self {
399      FontPaletteValuesProperty::FontFamily(f) => property!("font-family", f),
400      FontPaletteValuesProperty::BasePalette(b) => property!("base-palette", b),
401      FontPaletteValuesProperty::OverrideColors(o) => property!("override-colors", o),
402      FontPaletteValuesProperty::Custom(custom) => {
403        dest.write_str(custom.name.as_ref())?;
404        dest.delim(':', false)?;
405        custom.value.to_css(dest, true)
406      }
407    }
408  }
409}