Skip to main content

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    _declaration_start: &ParserState,
105  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
106    let state = input.state();
107    match_ignore_ascii_case! { &name,
108      "font-family" => {
109        // https://drafts.csswg.org/css-fonts-4/#font-family-2-desc
110        if let Ok(font_family) = FontFamily::parse(input) {
111          return match font_family {
112            FontFamily::Generic(_) => Err(input.new_custom_error(ParserError::InvalidDeclaration)),
113            _ => Ok(FontPaletteValuesProperty::FontFamily(font_family))
114          }
115        }
116      },
117      "base-palette" => {
118        // https://drafts.csswg.org/css-fonts-4/#base-palette-desc
119        if let Ok(base_palette) = BasePalette::parse(input) {
120          return Ok(FontPaletteValuesProperty::BasePalette(base_palette))
121        }
122      },
123      "override-colors" => {
124        // https://drafts.csswg.org/css-fonts-4/#override-color
125        if let Ok(override_colors) = input.parse_comma_separated(OverrideColors::parse) {
126          return Ok(FontPaletteValuesProperty::OverrideColors(override_colors))
127        }
128      },
129      _ => return Err(input.new_custom_error(ParserError::InvalidDeclaration))
130    }
131
132    input.reset(&state);
133    return Ok(FontPaletteValuesProperty::Custom(CustomProperty::parse(
134      name.into(),
135      input,
136      &ParserOptions::default(),
137    )?));
138  }
139}
140
141/// Default methods reject all at rules.
142impl<'i> AtRuleParser<'i> for FontPaletteValuesDeclarationParser {
143  type Prelude = ();
144  type AtRule = FontPaletteValuesProperty<'i>;
145  type Error = ParserError<'i>;
146}
147
148impl<'i> QualifiedRuleParser<'i> for FontPaletteValuesDeclarationParser {
149  type Prelude = ();
150  type QualifiedRule = FontPaletteValuesProperty<'i>;
151  type Error = ParserError<'i>;
152}
153
154impl<'i> RuleBodyItemParser<'i, FontPaletteValuesProperty<'i>, ParserError<'i>>
155  for FontPaletteValuesDeclarationParser
156{
157  fn parse_qualified(&self) -> bool {
158    false
159  }
160
161  fn parse_declarations(&self) -> bool {
162    true
163  }
164}
165
166impl<'i> FontPaletteValuesRule<'i> {
167  pub(crate) fn parse<'t>(
168    name: DashedIdent<'i>,
169    input: &mut Parser<'i, 't>,
170    loc: Location,
171  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
172    let mut decl_parser = FontPaletteValuesDeclarationParser;
173    let mut parser = RuleBodyParser::new(input, &mut decl_parser);
174    let mut properties = vec![];
175    while let Some(decl) = parser.next() {
176      if let Ok(decl) = decl {
177        properties.push(decl);
178      }
179    }
180
181    Ok(FontPaletteValuesRule { name, properties, loc })
182  }
183}
184
185impl<'i> Parse<'i> for BasePalette {
186  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
187    if let Ok(i) = input.try_parse(CSSInteger::parse) {
188      if i.is_negative() {
189        return Err(input.new_custom_error(ParserError::InvalidValue));
190      }
191      return Ok(BasePalette::Integer(i as u16));
192    }
193
194    let location = input.current_source_location();
195    let ident = input.expect_ident()?;
196    match_ignore_ascii_case! { &*ident,
197      "light" => Ok(BasePalette::Light),
198      "dark" => Ok(BasePalette::Dark),
199      _ => Err(location.new_unexpected_token_error(Token::Ident(ident.clone())))
200    }
201  }
202}
203
204impl ToCss for BasePalette {
205  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
206  where
207    W: std::fmt::Write,
208  {
209    match self {
210      BasePalette::Light => dest.write_str("light"),
211      BasePalette::Dark => dest.write_str("dark"),
212      BasePalette::Integer(i) => (*i as CSSInteger).to_css(dest),
213    }
214  }
215}
216
217impl<'i> Parse<'i> for OverrideColors {
218  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
219    let index = CSSInteger::parse(input)?;
220    if index.is_negative() {
221      return Err(input.new_custom_error(ParserError::InvalidValue));
222    }
223
224    let color = CssColor::parse(input)?;
225    if matches!(color, CssColor::CurrentColor) {
226      return Err(input.new_custom_error(ParserError::InvalidValue));
227    }
228
229    Ok(OverrideColors {
230      index: index as u16,
231      color,
232    })
233  }
234}
235
236impl ToCss for OverrideColors {
237  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
238  where
239    W: std::fmt::Write,
240  {
241    (self.index as CSSInteger).to_css(dest)?;
242    dest.write_char(' ')?;
243    self.color.to_css(dest)
244  }
245}
246
247impl OverrideColors {
248  fn get_fallback(&self, kind: ColorFallbackKind) -> OverrideColors {
249    OverrideColors {
250      index: self.index,
251      color: self.color.get_fallback(kind),
252    }
253  }
254}
255
256impl<'i> FontPaletteValuesRule<'i> {
257  pub(crate) fn minify(&mut self, context: &mut MinifyContext<'_, 'i>, _: bool) {
258    let mut properties = Vec::with_capacity(self.properties.len());
259    for property in &self.properties {
260      match property {
261        FontPaletteValuesProperty::OverrideColors(override_colors) => {
262          // Generate color fallbacks.
263          let mut fallbacks = ColorFallbackKind::empty();
264          for o in override_colors {
265            fallbacks |= o.color.get_necessary_fallbacks(context.targets.current);
266          }
267
268          if fallbacks.contains(ColorFallbackKind::RGB) {
269            properties.push(FontPaletteValuesProperty::OverrideColors(
270              override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::RGB)).collect(),
271            ));
272          }
273
274          if fallbacks.contains(ColorFallbackKind::P3) {
275            properties.push(FontPaletteValuesProperty::OverrideColors(
276              override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::P3)).collect(),
277            ));
278          }
279
280          let override_colors = if fallbacks.contains(ColorFallbackKind::LAB) {
281            override_colors.iter().map(|o| o.get_fallback(ColorFallbackKind::P3)).collect()
282          } else {
283            override_colors.clone()
284          };
285
286          properties.push(FontPaletteValuesProperty::OverrideColors(override_colors));
287        }
288        _ => properties.push(property.clone()),
289      }
290    }
291
292    self.properties = properties;
293  }
294
295  pub(crate) fn get_fallbacks<T>(&mut self, targets: Targets) -> Vec<CssRule<'i, T>> {
296    // Get fallbacks for unparsed properties. These will generate @supports rules
297    // containing duplicate @font-palette-values rules.
298    let mut fallbacks = ColorFallbackKind::empty();
299    for property in &self.properties {
300      match property {
301        FontPaletteValuesProperty::Custom(CustomProperty { value, .. }) => {
302          fallbacks |= value.get_necessary_fallbacks(targets);
303        }
304        _ => {}
305      }
306    }
307
308    let mut res = Vec::new();
309    let lowest_fallback = fallbacks.lowest();
310    fallbacks.remove(lowest_fallback);
311
312    if fallbacks.contains(ColorFallbackKind::P3) {
313      res.push(self.get_fallback(ColorFallbackKind::P3));
314    }
315
316    if fallbacks.contains(ColorFallbackKind::LAB)
317      || (!lowest_fallback.is_empty() && lowest_fallback != ColorFallbackKind::LAB)
318    {
319      res.push(self.get_fallback(ColorFallbackKind::LAB));
320    }
321
322    if !lowest_fallback.is_empty() {
323      for property in &mut self.properties {
324        match property {
325          FontPaletteValuesProperty::Custom(CustomProperty { value, .. }) => {
326            *value = value.get_fallback(lowest_fallback);
327          }
328          _ => {}
329        }
330      }
331    }
332
333    res
334  }
335
336  fn get_fallback<T>(&self, kind: ColorFallbackKind) -> CssRule<'i, T> {
337    let properties = self
338      .properties
339      .iter()
340      .map(|property| match property {
341        FontPaletteValuesProperty::Custom(custom) => FontPaletteValuesProperty::Custom(CustomProperty {
342          name: custom.name.clone(),
343          value: custom.value.get_fallback(kind),
344        }),
345        _ => property.clone(),
346      })
347      .collect();
348    CssRule::Supports(SupportsRule {
349      condition: kind.supports_condition(),
350      rules: CssRuleList(vec![CssRule::FontPaletteValues(FontPaletteValuesRule {
351        name: self.name.clone(),
352        properties,
353        loc: self.loc.clone(),
354      })]),
355      loc: self.loc.clone(),
356    })
357  }
358}
359
360impl<'i> ToCss for FontPaletteValuesRule<'i> {
361  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
362  where
363    W: std::fmt::Write,
364  {
365    #[cfg(feature = "sourcemap")]
366    dest.add_mapping(self.loc);
367    dest.write_str("@font-palette-values ")?;
368    self.name.to_css(dest)?;
369    dest.whitespace()?;
370    dest.write_char('{')?;
371    dest.indent();
372    let len = self.properties.len();
373    for (i, prop) in self.properties.iter().enumerate() {
374      dest.newline()?;
375      prop.to_css(dest)?;
376      if i != len - 1 || !dest.minify {
377        dest.write_char(';')?;
378      }
379    }
380    dest.dedent();
381    dest.newline()?;
382    dest.write_char('}')
383  }
384}
385
386impl<'i> ToCss for FontPaletteValuesProperty<'i> {
387  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
388  where
389    W: std::fmt::Write,
390  {
391    macro_rules! property {
392      ($prop: literal, $value: expr) => {{
393        dest.write_str($prop)?;
394        dest.delim(':', false)?;
395        $value.to_css(dest)
396      }};
397    }
398
399    match self {
400      FontPaletteValuesProperty::FontFamily(f) => property!("font-family", f),
401      FontPaletteValuesProperty::BasePalette(b) => property!("base-palette", b),
402      FontPaletteValuesProperty::OverrideColors(o) => property!("override-colors", o),
403      FontPaletteValuesProperty::Custom(custom) => {
404        dest.write_str(custom.name.as_ref())?;
405        dest.delim(':', false)?;
406        custom.value.to_css(dest, true)
407      }
408    }
409  }
410}