Skip to main content

lightningcss/properties/
font.rs

1//! CSS properties related to fonts.
2
3use std::collections::HashSet;
4
5use super::{Property, PropertyId};
6use crate::compat::Feature;
7use crate::context::PropertyHandlerContext;
8use crate::declaration::{DeclarationBlock, DeclarationList};
9use crate::error::{ParserError, PrinterError};
10use crate::macros::*;
11use crate::printer::Printer;
12use crate::targets::should_compile;
13use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
14use crate::values::length::LengthValue;
15use crate::values::number::CSSNumber;
16use crate::values::string::CowArcStr;
17use crate::values::{angle::Angle, length::LengthPercentage, percentage::Percentage};
18#[cfg(feature = "visitor")]
19use crate::visitor::Visit;
20use cssparser::*;
21
22/// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property.
23#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
24#[cfg_attr(feature = "visitor", derive(Visit))]
25#[cfg_attr(
26  feature = "serde",
27  derive(serde::Serialize, serde::Deserialize),
28  serde(tag = "type", content = "value", rename_all = "kebab-case")
29)]
30#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
31#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
32pub enum FontWeight {
33  /// An absolute font weight.
34  Absolute(AbsoluteFontWeight),
35  /// The `bolder` keyword.
36  Bolder,
37  /// The `lighter` keyword.
38  Lighter,
39}
40
41impl Default for FontWeight {
42  fn default() -> FontWeight {
43    FontWeight::Absolute(AbsoluteFontWeight::default())
44  }
45}
46
47impl IsCompatible for FontWeight {
48  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
49    match self {
50      FontWeight::Absolute(a) => a.is_compatible(browsers),
51      FontWeight::Bolder | FontWeight::Lighter => true,
52    }
53  }
54}
55
56/// An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values),
57/// as used in the `font-weight` property.
58///
59/// See [FontWeight](FontWeight).
60#[derive(Debug, Clone, PartialEq, Parse)]
61#[cfg_attr(feature = "visitor", derive(Visit))]
62#[cfg_attr(
63  feature = "serde",
64  derive(serde::Serialize, serde::Deserialize),
65  serde(tag = "type", content = "value", rename_all = "kebab-case")
66)]
67#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
68pub enum AbsoluteFontWeight {
69  /// An explicit weight.
70  Weight(CSSNumber),
71  /// Same as `400`.
72  Normal,
73  /// Same as `700`.
74  Bold,
75}
76
77impl Default for AbsoluteFontWeight {
78  fn default() -> AbsoluteFontWeight {
79    AbsoluteFontWeight::Normal
80  }
81}
82
83impl ToCss for AbsoluteFontWeight {
84  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
85  where
86    W: std::fmt::Write,
87  {
88    use AbsoluteFontWeight::*;
89    match self {
90      Weight(val) => val.to_css(dest),
91      Normal => dest.write_str(if dest.minify { "400" } else { "normal" }),
92      Bold => dest.write_str(if dest.minify { "700" } else { "bold" }),
93    }
94  }
95}
96
97impl IsCompatible for AbsoluteFontWeight {
98  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
99    match self {
100      // Older browsers only supported 100, 200, 300, ...900 rather than arbitrary values.
101      AbsoluteFontWeight::Weight(val) if !(*val >= 100.0 && *val <= 900.0 && *val % 100.0 == 0.0) => {
102        Feature::FontWeightNumber.is_compatible(browsers)
103      }
104      _ => true,
105    }
106  }
107}
108
109enum_property! {
110  /// An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value),
111  /// as used in the `font-size` property.
112  ///
113  /// See [FontSize](FontSize).
114  #[allow(missing_docs)]
115  pub enum AbsoluteFontSize {
116    "xx-small": XXSmall,
117    "x-small": XSmall,
118    "small": Small,
119    "medium": Medium,
120    "large": Large,
121    "x-large": XLarge,
122    "xx-large": XXLarge,
123    "xxx-large": XXXLarge,
124  }
125}
126
127impl IsCompatible for AbsoluteFontSize {
128  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
129    use AbsoluteFontSize::*;
130    match self {
131      XXXLarge => Feature::FontSizeXXXLarge.is_compatible(browsers),
132      _ => true,
133    }
134  }
135}
136
137enum_property! {
138  /// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value),
139  /// as used in the `font-size` property.
140  ///
141  /// See [FontSize](FontSize).
142  #[allow(missing_docs)]
143  pub enum RelativeFontSize {
144    Smaller,
145    Larger,
146  }
147}
148
149/// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property.
150#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
151#[cfg_attr(feature = "visitor", derive(Visit))]
152#[cfg_attr(
153  feature = "serde",
154  derive(serde::Serialize, serde::Deserialize),
155  serde(tag = "type", content = "value", rename_all = "kebab-case")
156)]
157#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
158#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
159pub enum FontSize {
160  /// An explicit size.
161  Length(LengthPercentage),
162  /// An absolute font size keyword.
163  Absolute(AbsoluteFontSize),
164  /// A relative font size keyword.
165  Relative(RelativeFontSize),
166}
167
168impl IsCompatible for FontSize {
169  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
170    match self {
171      FontSize::Length(LengthPercentage::Dimension(LengthValue::Rem(..))) => {
172        Feature::FontSizeRem.is_compatible(browsers)
173      }
174      FontSize::Length(l) => l.is_compatible(browsers),
175      FontSize::Absolute(a) => a.is_compatible(browsers),
176      FontSize::Relative(..) => true,
177    }
178  }
179}
180
181enum_property! {
182  /// A [font stretch keyword](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop),
183  /// as used in the `font-stretch` property.
184  ///
185  /// See [FontStretch](FontStretch).
186  pub enum FontStretchKeyword {
187    /// 100%
188    "normal": Normal,
189    /// 50%
190    "ultra-condensed": UltraCondensed,
191    /// 62.5%
192    "extra-condensed": ExtraCondensed,
193    /// 75%
194    "condensed": Condensed,
195    /// 87.5%
196    "semi-condensed": SemiCondensed,
197    /// 112.5%
198    "semi-expanded": SemiExpanded,
199    /// 125%
200    "expanded": Expanded,
201    /// 150%
202    "extra-expanded": ExtraExpanded,
203    /// 200%
204    "ultra-expanded": UltraExpanded,
205  }
206}
207
208impl Default for FontStretchKeyword {
209  fn default() -> FontStretchKeyword {
210    FontStretchKeyword::Normal
211  }
212}
213
214impl Into<Percentage> for &FontStretchKeyword {
215  fn into(self) -> Percentage {
216    use FontStretchKeyword::*;
217    let val = match self {
218      UltraCondensed => 0.5,
219      ExtraCondensed => 0.625,
220      Condensed => 0.75,
221      SemiCondensed => 0.875,
222      Normal => 1.0,
223      SemiExpanded => 1.125,
224      Expanded => 1.25,
225      ExtraExpanded => 1.5,
226      UltraExpanded => 2.0,
227    };
228    Percentage(val)
229  }
230}
231
232/// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property.
233#[derive(Debug, Clone, PartialEq, Parse)]
234#[cfg_attr(feature = "visitor", derive(Visit))]
235#[cfg_attr(
236  feature = "serde",
237  derive(serde::Serialize, serde::Deserialize),
238  serde(tag = "type", content = "value", rename_all = "kebab-case")
239)]
240#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
241#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
242pub enum FontStretch {
243  /// A font stretch keyword.
244  Keyword(FontStretchKeyword),
245  /// A percentage.
246  Percentage(Percentage),
247}
248
249impl Default for FontStretch {
250  fn default() -> FontStretch {
251    FontStretch::Keyword(FontStretchKeyword::default())
252  }
253}
254
255impl Into<Percentage> for &FontStretch {
256  fn into(self) -> Percentage {
257    match self {
258      FontStretch::Percentage(val) => val.clone(),
259      FontStretch::Keyword(keyword) => keyword.into(),
260    }
261  }
262}
263
264impl ToCss for FontStretch {
265  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
266  where
267    W: std::fmt::Write,
268  {
269    if dest.minify {
270      let percentage: Percentage = self.into();
271      return percentage.to_css(dest);
272    }
273
274    match self {
275      FontStretch::Percentage(val) => val.to_css(dest),
276      FontStretch::Keyword(val) => val.to_css(dest),
277    }
278  }
279}
280
281impl IsCompatible for FontStretch {
282  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
283    match self {
284      FontStretch::Percentage(..) => Feature::FontStretchPercentage.is_compatible(browsers),
285      FontStretch::Keyword(..) => true,
286    }
287  }
288}
289
290enum_property! {
291  /// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name,
292  /// as used in the `font-family` property.
293  ///
294  /// See [FontFamily](FontFamily).
295  #[allow(missing_docs)]
296  #[derive(Eq, Hash)]
297  pub enum GenericFontFamily {
298    "serif": Serif,
299    "sans-serif": SansSerif,
300    "cursive": Cursive,
301    "fantasy": Fantasy,
302    "monospace": Monospace,
303    "system-ui": SystemUI,
304    "emoji": Emoji,
305    "math": Math,
306    "fangsong": FangSong,
307    "ui-serif": UISerif,
308    "ui-sans-serif": UISansSerif,
309    "ui-monospace": UIMonospace,
310    "ui-rounded": UIRounded,
311
312    // CSS wide keywords. These must be parsed as identifiers so they
313    // don't get serialized as strings.
314    // https://www.w3.org/TR/css-values-4/#common-keywords
315    "initial": Initial,
316    "inherit": Inherit,
317    "unset": Unset,
318    // Default is also reserved by the <custom-ident> type.
319    // https://www.w3.org/TR/css-values-4/#custom-idents
320    "default": Default,
321
322    // CSS defaulting keywords
323    // https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
324    "revert": Revert,
325    "revert-layer": RevertLayer,
326  }
327}
328
329impl IsCompatible for GenericFontFamily {
330  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
331    use GenericFontFamily::*;
332    match self {
333      SystemUI => Feature::FontFamilySystemUi.is_compatible(browsers),
334      UISerif | UISansSerif | UIMonospace | UIRounded => Feature::ExtendedSystemFonts.is_compatible(browsers),
335      _ => true,
336    }
337  }
338}
339
340/// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property.
341#[derive(Debug, Clone, PartialEq, Eq, Hash)]
342#[cfg_attr(feature = "visitor", derive(Visit))]
343#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
344#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
345#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
346pub enum FontFamily<'i> {
347  /// A generic family name.
348  Generic(GenericFontFamily),
349  /// A custom family name.
350  #[cfg_attr(feature = "serde", serde(borrow))]
351  FamilyName(FamilyName<'i>),
352}
353
354impl<'i> Parse<'i> for FontFamily<'i> {
355  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
356    if let Ok(value) = input.try_parse(GenericFontFamily::parse) {
357      return Ok(FontFamily::Generic(value));
358    }
359
360    let family = FamilyName::parse(input)?;
361    Ok(FontFamily::FamilyName(family))
362  }
363}
364
365impl<'i> ToCss for FontFamily<'i> {
366  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
367  where
368    W: std::fmt::Write,
369  {
370    match self {
371      FontFamily::Generic(val) => val.to_css(dest),
372      FontFamily::FamilyName(val) => val.to_css(dest),
373    }
374  }
375}
376
377/// A font [family name](https://drafts.csswg.org/css-fonts/#family-name-syntax).
378#[derive(Debug, Clone, PartialEq, Eq, Hash)]
379#[cfg_attr(feature = "visitor", derive(Visit))]
380#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
381#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
382#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
383pub struct FamilyName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'i>);
384
385impl<'i> Parse<'i> for FamilyName<'i> {
386  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
387    if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) {
388      return Ok(FamilyName(value.into()));
389    }
390
391    let value: CowArcStr<'i> = input.expect_ident()?.into();
392    let mut string = None;
393    while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
394      if string.is_none() {
395        string = Some(value.to_string());
396      }
397
398      if let Some(string) = &mut string {
399        string.push(' ');
400        string.push_str(&ident);
401      }
402    }
403
404    let value = if let Some(string) = string {
405      string.into()
406    } else {
407      value
408    };
409
410    Ok(FamilyName(value))
411  }
412}
413
414impl<'i> ToCss for FamilyName<'i> {
415  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
416  where
417    W: std::fmt::Write,
418  {
419    // Generic family names such as sans-serif must be quoted if parsed as a string.
420    // CSS wide keywords, as well as "default", must also be quoted.
421    // https://www.w3.org/TR/css-fonts-4/#family-name-syntax
422    let val = &self.0;
423    if !val.is_empty() && !GenericFontFamily::parse_string(val).is_ok() {
424      // Family names with two or more consecutive spaces must be quoted to preserve the spaces.
425      let needs_quotes = val.contains("  ");
426      if needs_quotes {
427        serialize_string(&val, dest)?;
428        return Ok(());
429      }
430
431      let mut id = String::new();
432      let mut first = true;
433      for slice in val.split(' ') {
434        if first {
435          first = false;
436        } else {
437          id.push(' ');
438        }
439        serialize_identifier(slice, &mut id)?;
440      }
441      if id.len() < val.len() + 2 {
442        return dest.write_str(&id);
443      }
444    }
445    serialize_string(&val, dest)?;
446    Ok(())
447  }
448}
449
450impl IsCompatible for FontFamily<'_> {
451  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
452    match self {
453      FontFamily::Generic(g) => g.is_compatible(browsers),
454      FontFamily::FamilyName(..) => true,
455    }
456  }
457}
458
459/// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property.
460#[derive(Debug, Clone, PartialEq)]
461#[cfg_attr(feature = "visitor", derive(Visit))]
462#[cfg_attr(
463  feature = "serde",
464  derive(serde::Serialize, serde::Deserialize),
465  serde(tag = "type", content = "value", rename_all = "kebab-case")
466)]
467#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
468#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
469pub enum FontStyle {
470  /// Normal font style.
471  Normal,
472  /// Italic font style.
473  Italic,
474  /// Oblique font style, with a custom angle.
475  Oblique(#[cfg_attr(feature = "serde", serde(default = "FontStyle::default_oblique_angle"))] Angle),
476}
477
478impl Default for FontStyle {
479  fn default() -> FontStyle {
480    FontStyle::Normal
481  }
482}
483
484impl FontStyle {
485  #[inline]
486  pub(crate) fn default_oblique_angle() -> Angle {
487    Angle::Deg(14.0)
488  }
489}
490
491impl<'i> Parse<'i> for FontStyle {
492  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
493    let location = input.current_source_location();
494    let ident = input.expect_ident()?;
495    match_ignore_ascii_case! { &*ident,
496      "normal" => Ok(FontStyle::Normal),
497      "italic" => Ok(FontStyle::Italic),
498      "oblique" => {
499        let angle = input.try_parse(Angle::parse).unwrap_or(FontStyle::default_oblique_angle());
500        Ok(FontStyle::Oblique(angle))
501      },
502      _ => Err(location.new_unexpected_token_error(
503        cssparser::Token::Ident(ident.clone())
504      ))
505    }
506  }
507}
508
509impl ToCss for FontStyle {
510  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
511  where
512    W: std::fmt::Write,
513  {
514    match self {
515      FontStyle::Normal => dest.write_str("normal"),
516      FontStyle::Italic => dest.write_str("italic"),
517      FontStyle::Oblique(angle) => {
518        dest.write_str("oblique")?;
519        if *angle != FontStyle::default_oblique_angle() {
520          dest.write_char(' ')?;
521          angle.to_css(dest)?;
522        }
523        Ok(())
524      }
525    }
526  }
527}
528
529impl IsCompatible for FontStyle {
530  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
531    match self {
532      FontStyle::Oblique(angle) if *angle != FontStyle::default_oblique_angle() => {
533        Feature::FontStyleObliqueAngle.is_compatible(browsers)
534      }
535      FontStyle::Normal | FontStyle::Italic | FontStyle::Oblique(..) => true,
536    }
537  }
538}
539
540enum_property! {
541  /// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property.
542  pub enum FontVariantCaps {
543    /// No special capitalization features are applied.
544    Normal,
545    /// The small capitals feature is used for lower case letters.
546    SmallCaps,
547    /// Small capitals are used for both upper and lower case letters.
548    AllSmallCaps,
549    /// Petite capitals are used.
550    PetiteCaps,
551    /// Petite capitals are used for both upper and lower case letters.
552    AllPetiteCaps,
553    /// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters.
554    Unicase,
555    /// Uses titling capitals.
556    TitlingCaps,
557  }
558}
559
560impl Default for FontVariantCaps {
561  fn default() -> FontVariantCaps {
562    FontVariantCaps::Normal
563  }
564}
565
566impl FontVariantCaps {
567  fn is_css2(&self) -> bool {
568    matches!(self, FontVariantCaps::Normal | FontVariantCaps::SmallCaps)
569  }
570
571  fn parse_css2<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
572    let value = Self::parse(input)?;
573    if !value.is_css2() {
574      return Err(input.new_custom_error(ParserError::InvalidValue));
575    }
576    Ok(value)
577  }
578}
579
580impl IsCompatible for FontVariantCaps {
581  fn is_compatible(&self, _browsers: crate::targets::Browsers) -> bool {
582    true
583  }
584}
585
586/// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property.
587#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
588#[cfg_attr(feature = "visitor", derive(Visit))]
589#[cfg_attr(
590  feature = "serde",
591  derive(serde::Serialize, serde::Deserialize),
592  serde(tag = "type", content = "value", rename_all = "kebab-case")
593)]
594#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
595#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
596pub enum LineHeight {
597  /// The UA sets the line height based on the font.
598  Normal,
599  /// A multiple of the element's font size.
600  Number(CSSNumber),
601  /// An explicit height.
602  Length(LengthPercentage),
603}
604
605impl Default for LineHeight {
606  fn default() -> LineHeight {
607    LineHeight::Normal
608  }
609}
610
611impl IsCompatible for LineHeight {
612  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
613    match self {
614      LineHeight::Length(l) => l.is_compatible(browsers),
615      LineHeight::Normal | LineHeight::Number(..) => true,
616    }
617  }
618}
619
620enum_property! {
621  /// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
622  pub enum VerticalAlignKeyword {
623    /// Align the baseline of the box with the baseline of the parent box.
624    Baseline,
625    /// Lower the baseline of the box to the proper position for subscripts of the parent’s box.
626    Sub,
627    /// Raise the baseline of the box to the proper position for superscripts of the parent’s box.
628    Super,
629    /// Align the top of the aligned subtree with the top of the line box.
630    Top,
631    /// Align the top of the box with the top of the parent’s content area.
632    TextTop,
633    /// Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent.
634    Middle,
635    /// Align the bottom of the aligned subtree with the bottom of the line box.
636    Bottom,
637    /// Align the bottom of the box with the bottom of the parent’s content area.
638    TextBottom,
639  }
640}
641
642/// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
643// TODO: there is a more extensive spec in CSS3 but it doesn't seem any browser implements it? https://www.w3.org/TR/css-inline-3/#transverse-alignment
644#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
645#[cfg_attr(feature = "visitor", derive(Visit))]
646#[cfg_attr(
647  feature = "serde",
648  derive(serde::Serialize, serde::Deserialize),
649  serde(tag = "type", content = "value", rename_all = "kebab-case")
650)]
651#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
652#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
653pub enum VerticalAlign {
654  /// A vertical align keyword.
655  Keyword(VerticalAlignKeyword),
656  /// An explicit length.
657  Length(LengthPercentage),
658}
659
660define_shorthand! {
661  /// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property.
662  pub struct Font<'i> {
663    /// The font family.
664    #[cfg_attr(feature = "serde", serde(borrow))]
665    family: FontFamily(Vec<FontFamily<'i>>),
666    /// The font size.
667    size: FontSize(FontSize),
668    /// The font style.
669    style: FontStyle(FontStyle),
670    /// The font weight.
671    weight: FontWeight(FontWeight),
672    /// The font stretch.
673    stretch: FontStretch(FontStretch),
674    /// The line height.
675    line_height: LineHeight(LineHeight),
676    /// How the text should be capitalized. Only CSS 2.1 values are supported.
677    variant_caps: FontVariantCaps(FontVariantCaps),
678  }
679}
680
681impl<'i> Parse<'i> for Font<'i> {
682  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
683    let mut style = None;
684    let mut weight = None;
685    let mut stretch = None;
686    let size;
687    let mut variant_caps = None;
688    let mut count = 0;
689
690    loop {
691      // Skip "normal" since it is valid for several properties, but we don't know which ones it will be used for yet.
692      if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
693        count += 1;
694        continue;
695      }
696      if style.is_none() {
697        if let Ok(value) = input.try_parse(FontStyle::parse) {
698          style = Some(value);
699          count += 1;
700          continue;
701        }
702      }
703      if weight.is_none() {
704        if let Ok(value) = input.try_parse(FontWeight::parse) {
705          weight = Some(value);
706          count += 1;
707          continue;
708        }
709      }
710      if variant_caps.is_none() {
711        if let Ok(value) = input.try_parse(FontVariantCaps::parse_css2) {
712          variant_caps = Some(value);
713          count += 1;
714          continue;
715        }
716      }
717
718      if stretch.is_none() {
719        if let Ok(value) = input.try_parse(FontStretchKeyword::parse) {
720          stretch = Some(FontStretch::Keyword(value));
721          count += 1;
722          continue;
723        }
724      }
725      size = Some(FontSize::parse(input)?);
726      break;
727    }
728
729    if count > 4 {
730      return Err(input.new_custom_error(ParserError::InvalidDeclaration));
731    }
732
733    let size = match size {
734      Some(s) => s,
735      None => return Err(input.new_custom_error(ParserError::InvalidDeclaration)),
736    };
737
738    let line_height = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
739      Some(LineHeight::parse(input)?)
740    } else {
741      None
742    };
743
744    let family = input.parse_comma_separated(FontFamily::parse)?;
745    Ok(Font {
746      family,
747      size,
748      style: style.unwrap_or_default(),
749      weight: weight.unwrap_or_default(),
750      stretch: stretch.unwrap_or_default(),
751      line_height: line_height.unwrap_or_default(),
752      variant_caps: variant_caps.unwrap_or_default(),
753    })
754  }
755}
756
757impl<'i> ToCss for Font<'i> {
758  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
759  where
760    W: std::fmt::Write,
761  {
762    if self.style != FontStyle::default() {
763      self.style.to_css(dest)?;
764      dest.write_char(' ')?;
765    }
766
767    if self.variant_caps != FontVariantCaps::default() {
768      self.variant_caps.to_css(dest)?;
769      dest.write_char(' ')?;
770    }
771
772    if self.weight != FontWeight::default() {
773      self.weight.to_css(dest)?;
774      dest.write_char(' ')?;
775    }
776
777    if self.stretch != FontStretch::default() {
778      self.stretch.to_css(dest)?;
779      dest.write_char(' ')?;
780    }
781
782    self.size.to_css(dest)?;
783
784    if self.line_height != LineHeight::default() {
785      dest.delim('/', true)?;
786      self.line_height.to_css(dest)?;
787    }
788
789    dest.write_char(' ')?;
790
791    let len = self.family.len();
792    for (idx, val) in self.family.iter().enumerate() {
793      val.to_css(dest)?;
794      if idx < len - 1 {
795        dest.delim(',', false)?;
796      }
797    }
798
799    Ok(())
800  }
801}
802
803property_bitflags! {
804  #[derive(Default, Debug)]
805  struct FontProperty: u8 {
806    const FontFamily = 1 << 0;
807    const FontSize = 1 << 1;
808    const FontStyle = 1 << 2;
809    const FontWeight = 1 << 3;
810    const FontStretch = 1 << 4;
811    const LineHeight = 1 << 5;
812    const FontVariantCaps = 1 << 6;
813    const Font = Self::FontFamily.bits() | Self::FontSize.bits() | Self::FontStyle.bits() | Self::FontWeight.bits() | Self::FontStretch.bits() | Self::LineHeight.bits() | Self::FontVariantCaps.bits();
814  }
815}
816
817#[derive(Default, Debug)]
818pub(crate) struct FontHandler<'i> {
819  family: Option<Vec<FontFamily<'i>>>,
820  size: Option<FontSize>,
821  style: Option<FontStyle>,
822  weight: Option<FontWeight>,
823  stretch: Option<FontStretch>,
824  line_height: Option<LineHeight>,
825  variant_caps: Option<FontVariantCaps>,
826  flushed_properties: FontProperty,
827  has_any: bool,
828}
829
830impl<'i> PropertyHandler<'i> for FontHandler<'i> {
831  fn handle_property(
832    &mut self,
833    property: &Property<'i>,
834    dest: &mut DeclarationList<'i>,
835    context: &mut PropertyHandlerContext<'i, '_>,
836  ) -> bool {
837    use Property::*;
838
839    macro_rules! flush {
840      ($prop: ident, $val: expr) => {{
841        if self.$prop.is_some() && self.$prop.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) {
842          self.flush(dest, context);
843        }
844      }};
845    }
846
847    macro_rules! property {
848      ($prop: ident, $val: ident) => {{
849        flush!($prop, $val);
850        self.$prop = Some($val.clone());
851        self.has_any = true;
852      }};
853    }
854
855    match property {
856      FontFamily(val) => property!(family, val),
857      FontSize(val) => property!(size, val),
858      FontStyle(val) => property!(style, val),
859      FontWeight(val) => property!(weight, val),
860      FontStretch(val) => property!(stretch, val),
861      FontVariantCaps(val) => property!(variant_caps, val),
862      LineHeight(val) => property!(line_height, val),
863      Font(val) => {
864        flush!(family, &val.family);
865        flush!(size, &val.size);
866        flush!(style, &val.style);
867        flush!(weight, &val.weight);
868        flush!(stretch, &val.stretch);
869        flush!(line_height, &val.line_height);
870        flush!(variant_caps, &val.variant_caps);
871        self.family = Some(val.family.clone());
872        self.size = Some(val.size.clone());
873        self.style = Some(val.style.clone());
874        self.weight = Some(val.weight.clone());
875        self.stretch = Some(val.stretch.clone());
876        self.line_height = Some(val.line_height.clone());
877        self.variant_caps = Some(val.variant_caps.clone());
878        self.has_any = true;
879        // TODO: reset other properties
880      }
881      Unparsed(val) if is_font_property(&val.property_id) => {
882        self.flush(dest, context);
883        self
884          .flushed_properties
885          .insert(FontProperty::try_from(&val.property_id).unwrap());
886        dest.push(property.clone());
887      }
888      _ => return false,
889    }
890
891    true
892  }
893
894  fn finalize(&mut self, decls: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
895    self.flush(decls, context);
896    self.flushed_properties = FontProperty::empty();
897  }
898}
899
900impl<'i> FontHandler<'i> {
901  fn flush(&mut self, decls: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
902    if !self.has_any {
903      return;
904    }
905
906    self.has_any = false;
907
908    macro_rules! push {
909      ($prop: ident, $val: expr) => {
910        decls.push(Property::$prop($val));
911        self.flushed_properties.insert(FontProperty::$prop);
912      };
913    }
914
915    let mut family = std::mem::take(&mut self.family);
916    if !self.flushed_properties.contains(FontProperty::FontFamily) {
917      family = compatible_font_family(family, !should_compile!(context.targets, FontFamilySystemUi));
918    }
919    let size = std::mem::take(&mut self.size);
920    let style = std::mem::take(&mut self.style);
921    let weight = std::mem::take(&mut self.weight);
922    let stretch = std::mem::take(&mut self.stretch);
923    let line_height = std::mem::take(&mut self.line_height);
924    let variant_caps = std::mem::take(&mut self.variant_caps);
925
926    if let Some(family) = &mut family {
927      if family.len() > 1 {
928        // Dedupe.
929        let mut seen = HashSet::new();
930        family.retain(|f| seen.insert(f.clone()));
931      }
932    }
933
934    if family.is_some()
935      && size.is_some()
936      && style.is_some()
937      && weight.is_some()
938      && stretch.is_some()
939      && line_height.is_some()
940      && variant_caps.is_some()
941    {
942      let caps = variant_caps.unwrap();
943      push!(
944        Font,
945        Font {
946          family: family.unwrap(),
947          size: size.unwrap(),
948          style: style.unwrap(),
949          weight: weight.unwrap(),
950          stretch: stretch.unwrap(),
951          line_height: line_height.unwrap(),
952          variant_caps: if caps.is_css2() {
953            caps
954          } else {
955            FontVariantCaps::default()
956          },
957        }
958      );
959
960      // The `font` property only accepts CSS 2.1 values for font-variant caps.
961      // If we have a CSS 3+ value, we need to add a separate property.
962      if !caps.is_css2() {
963        push!(FontVariantCaps, variant_caps.unwrap());
964      }
965    } else {
966      if let Some(val) = family {
967        push!(FontFamily, val);
968      }
969
970      if let Some(val) = size {
971        push!(FontSize, val);
972      }
973
974      if let Some(val) = style {
975        push!(FontStyle, val);
976      }
977
978      if let Some(val) = variant_caps {
979        push!(FontVariantCaps, val);
980      }
981
982      if let Some(val) = weight {
983        push!(FontWeight, val);
984      }
985
986      if let Some(val) = stretch {
987        push!(FontStretch, val);
988      }
989
990      if let Some(val) = line_height {
991        push!(LineHeight, val);
992      }
993    }
994  }
995}
996
997const SYSTEM_UI: FontFamily = FontFamily::Generic(GenericFontFamily::SystemUI);
998
999const DEFAULT_SYSTEM_FONTS: &[&str] = &[
1000  // #1: Supported as the '-apple-system' value (macOS, Safari >= 9.2 < 11, Firefox >= 43)
1001  "-apple-system",
1002  // #2: Supported as the 'BlinkMacSystemFont' value (macOS, Chrome < 56)
1003  "BlinkMacSystemFont",
1004  "Segoe UI",  // Windows >= Vista
1005  "Roboto",    // Android >= 4
1006  "Noto Sans", // Plasma >= 5.5
1007  "Ubuntu",    // Ubuntu >= 10.10
1008  "Cantarell", // GNOME >= 3
1009  "Helvetica Neue",
1010];
1011
1012/// [`system-ui`](https://www.w3.org/TR/css-fonts-4/#system-ui-def) is a special generic font family
1013/// It is platform dependent but if not supported by the target will simply be ignored
1014/// This list is an attempt at providing that support
1015#[inline]
1016fn compatible_font_family(mut family: Option<Vec<FontFamily>>, is_supported: bool) -> Option<Vec<FontFamily>> {
1017  if is_supported {
1018    return family;
1019  }
1020
1021  if let Some(families) = &mut family {
1022    if let Some(position) = families.iter().position(|v| *v == SYSTEM_UI) {
1023      families.splice(
1024        (position + 1)..(position + 1),
1025        DEFAULT_SYSTEM_FONTS
1026          .iter()
1027          .map(|name| FontFamily::FamilyName(FamilyName(CowArcStr::from(*name)))),
1028      );
1029    }
1030  }
1031
1032  return family;
1033}
1034
1035#[inline]
1036fn is_font_property(property_id: &PropertyId) -> bool {
1037  match property_id {
1038    PropertyId::FontFamily
1039    | PropertyId::FontSize
1040    | PropertyId::FontStyle
1041    | PropertyId::FontWeight
1042    | PropertyId::FontStretch
1043    | PropertyId::FontVariantCaps
1044    | PropertyId::LineHeight
1045    | PropertyId::Font => true,
1046    _ => false,
1047  }
1048}