| //! CSS properties related to fonts. |
| |
| use std::collections::HashSet; |
| |
| use super::{Property, PropertyId}; |
| use crate::compat::Feature; |
| use crate::context::PropertyHandlerContext; |
| use crate::declaration::{DeclarationBlock, DeclarationList}; |
| use crate::error::{ParserError, PrinterError}; |
| use crate::macros::*; |
| use crate::printer::Printer; |
| use crate::targets::should_compile; |
| use crate::traits::{IsCompatible, Parse, PropertyHandler, Shorthand, ToCss}; |
| use crate::values::length::LengthValue; |
| use crate::values::number::CSSNumber; |
| use crate::values::string::CowArcStr; |
| use crate::values::{angle::Angle, length::LengthPercentage, percentage::Percentage}; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use cssparser::*; |
| |
| /// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum FontWeight { |
| /// An absolute font weight. |
| Absolute(AbsoluteFontWeight), |
| /// The `bolder` keyword. |
| Bolder, |
| /// The `lighter` keyword. |
| Lighter, |
| } |
| |
| impl Default for FontWeight { |
| fn default() -> FontWeight { |
| FontWeight::Absolute(AbsoluteFontWeight::default()) |
| } |
| } |
| |
| impl IsCompatible for FontWeight { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| FontWeight::Absolute(a) => a.is_compatible(browsers), |
| FontWeight::Bolder | FontWeight::Lighter => true, |
| } |
| } |
| } |
| |
| /// An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values), |
| /// as used in the `font-weight` property. |
| /// |
| /// See [FontWeight](FontWeight). |
| #[derive(Debug, Clone, PartialEq, Parse)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum AbsoluteFontWeight { |
| /// An explicit weight. |
| Weight(CSSNumber), |
| /// Same as `400`. |
| Normal, |
| /// Same as `700`. |
| Bold, |
| } |
| |
| impl Default for AbsoluteFontWeight { |
| fn default() -> AbsoluteFontWeight { |
| AbsoluteFontWeight::Normal |
| } |
| } |
| |
| impl ToCss for AbsoluteFontWeight { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| use AbsoluteFontWeight::*; |
| match self { |
| Weight(val) => val.to_css(dest), |
| Normal => dest.write_str(if dest.minify { "400" } else { "normal" }), |
| Bold => dest.write_str(if dest.minify { "700" } else { "bold" }), |
| } |
| } |
| } |
| |
| impl IsCompatible for AbsoluteFontWeight { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| // Older browsers only supported 100, 200, 300, ...900 rather than arbitrary values. |
| AbsoluteFontWeight::Weight(val) if !(*val >= 100.0 && *val <= 900.0 && *val % 100.0 == 0.0) => { |
| Feature::FontWeightNumber.is_compatible(browsers) |
| } |
| _ => true, |
| } |
| } |
| } |
| |
| enum_property! { |
| /// An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value), |
| /// as used in the `font-size` property. |
| /// |
| /// See [FontSize](FontSize). |
| #[allow(missing_docs)] |
| pub enum AbsoluteFontSize { |
| "xx-small": XXSmall, |
| "x-small": XSmall, |
| "small": Small, |
| "medium": Medium, |
| "large": Large, |
| "x-large": XLarge, |
| "xx-large": XXLarge, |
| "xxx-large": XXXLarge, |
| } |
| } |
| |
| impl IsCompatible for AbsoluteFontSize { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| use AbsoluteFontSize::*; |
| match self { |
| XXXLarge => Feature::FontSizeXXXLarge.is_compatible(browsers), |
| _ => true, |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value), |
| /// as used in the `font-size` property. |
| /// |
| /// See [FontSize](FontSize). |
| #[allow(missing_docs)] |
| pub enum RelativeFontSize { |
| Smaller, |
| Larger, |
| } |
| } |
| |
| /// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum FontSize { |
| /// An explicit size. |
| Length(LengthPercentage), |
| /// An absolute font size keyword. |
| Absolute(AbsoluteFontSize), |
| /// A relative font size keyword. |
| Relative(RelativeFontSize), |
| } |
| |
| impl IsCompatible for FontSize { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| FontSize::Length(LengthPercentage::Dimension(LengthValue::Rem(..))) => { |
| Feature::FontSizeRem.is_compatible(browsers) |
| } |
| FontSize::Length(l) => l.is_compatible(browsers), |
| FontSize::Absolute(a) => a.is_compatible(browsers), |
| FontSize::Relative(..) => true, |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A [font stretch keyword](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop), |
| /// as used in the `font-stretch` property. |
| /// |
| /// See [FontStretch](FontStretch). |
| pub enum FontStretchKeyword { |
| /// 100% |
| "normal": Normal, |
| /// 50% |
| "ultra-condensed": UltraCondensed, |
| /// 62.5% |
| "extra-condensed": ExtraCondensed, |
| /// 75% |
| "condensed": Condensed, |
| /// 87.5% |
| "semi-condensed": SemiCondensed, |
| /// 112.5% |
| "semi-expanded": SemiExpanded, |
| /// 125% |
| "expanded": Expanded, |
| /// 150% |
| "extra-expanded": ExtraExpanded, |
| /// 200% |
| "ultra-expanded": UltraExpanded, |
| } |
| } |
| |
| impl Default for FontStretchKeyword { |
| fn default() -> FontStretchKeyword { |
| FontStretchKeyword::Normal |
| } |
| } |
| |
| impl Into<Percentage> for &FontStretchKeyword { |
| fn into(self) -> Percentage { |
| use FontStretchKeyword::*; |
| let val = match self { |
| UltraCondensed => 0.5, |
| ExtraCondensed => 0.625, |
| Condensed => 0.75, |
| SemiCondensed => 0.875, |
| Normal => 1.0, |
| SemiExpanded => 1.125, |
| Expanded => 1.25, |
| ExtraExpanded => 1.5, |
| UltraExpanded => 2.0, |
| }; |
| Percentage(val) |
| } |
| } |
| |
| /// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property. |
| #[derive(Debug, Clone, PartialEq, Parse)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum FontStretch { |
| /// A font stretch keyword. |
| Keyword(FontStretchKeyword), |
| /// A percentage. |
| Percentage(Percentage), |
| } |
| |
| impl Default for FontStretch { |
| fn default() -> FontStretch { |
| FontStretch::Keyword(FontStretchKeyword::default()) |
| } |
| } |
| |
| impl Into<Percentage> for &FontStretch { |
| fn into(self) -> Percentage { |
| match self { |
| FontStretch::Percentage(val) => val.clone(), |
| FontStretch::Keyword(keyword) => keyword.into(), |
| } |
| } |
| } |
| |
| impl ToCss for FontStretch { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if dest.minify { |
| let percentage: Percentage = self.into(); |
| return percentage.to_css(dest); |
| } |
| |
| match self { |
| FontStretch::Percentage(val) => val.to_css(dest), |
| FontStretch::Keyword(val) => val.to_css(dest), |
| } |
| } |
| } |
| |
| impl IsCompatible for FontStretch { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| FontStretch::Percentage(..) => Feature::FontStretchPercentage.is_compatible(browsers), |
| FontStretch::Keyword(..) => true, |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name, |
| /// as used in the `font-family` property. |
| /// |
| /// See [FontFamily](FontFamily). |
| #[allow(missing_docs)] |
| #[derive(Eq, Hash)] |
| pub enum GenericFontFamily { |
| "serif": Serif, |
| "sans-serif": SansSerif, |
| "cursive": Cursive, |
| "fantasy": Fantasy, |
| "monospace": Monospace, |
| "system-ui": SystemUI, |
| "emoji": Emoji, |
| "math": Math, |
| "fangsong": FangSong, |
| "ui-serif": UISerif, |
| "ui-sans-serif": UISansSerif, |
| "ui-monospace": UIMonospace, |
| "ui-rounded": UIRounded, |
| |
| // CSS wide keywords. These must be parsed as identifiers so they |
| // don't get serialized as strings. |
| // https://www.w3.org/TR/css-values-4/#common-keywords |
| "initial": Initial, |
| "inherit": Inherit, |
| "unset": Unset, |
| // Default is also reserved by the <custom-ident> type. |
| // https://www.w3.org/TR/css-values-4/#custom-idents |
| "default": Default, |
| |
| // CSS defaulting keywords |
| // https://drafts.csswg.org/css-cascade-5/#defaulting-keywords |
| "revert": Revert, |
| "revert-layer": RevertLayer, |
| } |
| } |
| |
| impl IsCompatible for GenericFontFamily { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| use GenericFontFamily::*; |
| match self { |
| SystemUI => Feature::FontFamilySystemUi.is_compatible(browsers), |
| UISerif | UISansSerif | UIMonospace | UIRounded => Feature::ExtendedSystemFonts.is_compatible(browsers), |
| _ => true, |
| } |
| } |
| } |
| |
| /// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property. |
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum FontFamily<'i> { |
| /// A generic family name. |
| Generic(GenericFontFamily), |
| /// A custom family name. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| FamilyName(FamilyName<'i>), |
| } |
| |
| impl<'i> Parse<'i> for FontFamily<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if let Ok(value) = input.try_parse(GenericFontFamily::parse) { |
| return Ok(FontFamily::Generic(value)); |
| } |
| |
| let family = FamilyName::parse(input)?; |
| Ok(FontFamily::FamilyName(family)) |
| } |
| } |
| |
| impl<'i> ToCss for FontFamily<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| FontFamily::Generic(val) => val.to_css(dest), |
| FontFamily::FamilyName(val) => val.to_css(dest), |
| } |
| } |
| } |
| |
| /// A font [family name](https://drafts.csswg.org/css-fonts/#family-name-syntax). |
| #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub struct FamilyName<'i>(#[cfg_attr(feature = "serde", serde(borrow))] CowArcStr<'i>); |
| |
| impl<'i> Parse<'i> for FamilyName<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) { |
| return Ok(FamilyName(value.into())); |
| } |
| |
| let value: CowArcStr<'i> = input.expect_ident()?.into(); |
| let mut string = None; |
| while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { |
| if string.is_none() { |
| string = Some(value.to_string()); |
| } |
| |
| if let Some(string) = &mut string { |
| string.push(' '); |
| string.push_str(&ident); |
| } |
| } |
| |
| let value = if let Some(string) = string { |
| string.into() |
| } else { |
| value |
| }; |
| |
| Ok(FamilyName(value)) |
| } |
| } |
| |
| impl<'i> ToCss for FamilyName<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| // Generic family names such as sans-serif must be quoted if parsed as a string. |
| // CSS wide keywords, as well as "default", must also be quoted. |
| // https://www.w3.org/TR/css-fonts-4/#family-name-syntax |
| let val = &self.0; |
| if !val.is_empty() && !GenericFontFamily::parse_string(val).is_ok() { |
| let mut id = String::new(); |
| let mut first = true; |
| for slice in val.split(' ') { |
| if first { |
| first = false; |
| } else { |
| id.push(' '); |
| } |
| serialize_identifier(slice, &mut id)?; |
| } |
| if id.len() < val.len() + 2 { |
| return dest.write_str(&id); |
| } |
| } |
| serialize_string(&val, dest)?; |
| Ok(()) |
| } |
| } |
| |
| impl IsCompatible for FontFamily<'_> { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| FontFamily::Generic(g) => g.is_compatible(browsers), |
| FontFamily::FamilyName(..) => true, |
| } |
| } |
| } |
| |
| /// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum FontStyle { |
| /// Normal font style. |
| Normal, |
| /// Italic font style. |
| Italic, |
| /// Oblique font style, with a custom angle. |
| Oblique(#[cfg_attr(feature = "serde", serde(default = "FontStyle::default_oblique_angle"))] Angle), |
| } |
| |
| impl Default for FontStyle { |
| fn default() -> FontStyle { |
| FontStyle::Normal |
| } |
| } |
| |
| impl FontStyle { |
| #[inline] |
| pub(crate) fn default_oblique_angle() -> Angle { |
| Angle::Deg(14.0) |
| } |
| } |
| |
| impl<'i> Parse<'i> for FontStyle { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let location = input.current_source_location(); |
| let ident = input.expect_ident()?; |
| match_ignore_ascii_case! { &*ident, |
| "normal" => Ok(FontStyle::Normal), |
| "italic" => Ok(FontStyle::Italic), |
| "oblique" => { |
| let angle = input.try_parse(Angle::parse).unwrap_or(FontStyle::default_oblique_angle()); |
| Ok(FontStyle::Oblique(angle)) |
| }, |
| _ => Err(location.new_unexpected_token_error( |
| cssparser::Token::Ident(ident.clone()) |
| )) |
| } |
| } |
| } |
| |
| impl ToCss for FontStyle { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| FontStyle::Normal => dest.write_str("normal"), |
| FontStyle::Italic => dest.write_str("italic"), |
| FontStyle::Oblique(angle) => { |
| dest.write_str("oblique")?; |
| if *angle != FontStyle::default_oblique_angle() { |
| dest.write_char(' ')?; |
| angle.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| impl IsCompatible for FontStyle { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| FontStyle::Oblique(angle) if *angle != FontStyle::default_oblique_angle() => { |
| Feature::FontStyleObliqueAngle.is_compatible(browsers) |
| } |
| FontStyle::Normal | FontStyle::Italic | FontStyle::Oblique(..) => true, |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property. |
| pub enum FontVariantCaps { |
| /// No special capitalization features are applied. |
| Normal, |
| /// The small capitals feature is used for lower case letters. |
| SmallCaps, |
| /// Small capitals are used for both upper and lower case letters. |
| AllSmallCaps, |
| /// Petite capitals are used. |
| PetiteCaps, |
| /// Petite capitals are used for both upper and lower case letters. |
| AllPetiteCaps, |
| /// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters. |
| Unicase, |
| /// Uses titling capitals. |
| TitlingCaps, |
| } |
| } |
| |
| impl Default for FontVariantCaps { |
| fn default() -> FontVariantCaps { |
| FontVariantCaps::Normal |
| } |
| } |
| |
| impl FontVariantCaps { |
| fn is_css2(&self) -> bool { |
| matches!(self, FontVariantCaps::Normal | FontVariantCaps::SmallCaps) |
| } |
| |
| fn parse_css2<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let value = Self::parse(input)?; |
| if !value.is_css2() { |
| return Err(input.new_custom_error(ParserError::InvalidValue)); |
| } |
| Ok(value) |
| } |
| } |
| |
| impl IsCompatible for FontVariantCaps { |
| fn is_compatible(&self, _browsers: crate::targets::Browsers) -> bool { |
| true |
| } |
| } |
| |
| /// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum LineHeight { |
| /// The UA sets the line height based on the font. |
| Normal, |
| /// A multiple of the element's font size. |
| Number(CSSNumber), |
| /// An explicit height. |
| Length(LengthPercentage), |
| } |
| |
| impl Default for LineHeight { |
| fn default() -> LineHeight { |
| LineHeight::Normal |
| } |
| } |
| |
| impl IsCompatible for LineHeight { |
| fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool { |
| match self { |
| LineHeight::Length(l) => l.is_compatible(browsers), |
| LineHeight::Normal | LineHeight::Number(..) => true, |
| } |
| } |
| } |
| |
| enum_property! { |
| /// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. |
| pub enum VerticalAlignKeyword { |
| /// Align the baseline of the box with the baseline of the parent box. |
| Baseline, |
| /// Lower the baseline of the box to the proper position for subscripts of the parent’s box. |
| Sub, |
| /// Raise the baseline of the box to the proper position for superscripts of the parent’s box. |
| Super, |
| /// Align the top of the aligned subtree with the top of the line box. |
| Top, |
| /// Align the top of the box with the top of the parent’s content area. |
| TextTop, |
| /// Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. |
| Middle, |
| /// Align the bottom of the aligned subtree with the bottom of the line box. |
| Bottom, |
| /// Align the bottom of the box with the bottom of the parent’s content area. |
| TextBottom, |
| } |
| } |
| |
| /// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property. |
| // 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 |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum VerticalAlign { |
| /// A vertical align keyword. |
| Keyword(VerticalAlignKeyword), |
| /// An explicit length. |
| Length(LengthPercentage), |
| } |
| |
| define_shorthand! { |
| /// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property. |
| pub struct Font<'i> { |
| /// The font family. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| family: FontFamily(Vec<FontFamily<'i>>), |
| /// The font size. |
| size: FontSize(FontSize), |
| /// The font style. |
| style: FontStyle(FontStyle), |
| /// The font weight. |
| weight: FontWeight(FontWeight), |
| /// The font stretch. |
| stretch: FontStretch(FontStretch), |
| /// The line height. |
| line_height: LineHeight(LineHeight), |
| /// How the text should be capitalized. Only CSS 2.1 values are supported. |
| variant_caps: FontVariantCaps(FontVariantCaps), |
| } |
| } |
| |
| impl<'i> Parse<'i> for Font<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut style = None; |
| let mut weight = None; |
| let mut stretch = None; |
| let size; |
| let mut variant_caps = None; |
| let mut count = 0; |
| |
| loop { |
| // Skip "normal" since it is valid for several properties, but we don't know which ones it will be used for yet. |
| if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { |
| count += 1; |
| continue; |
| } |
| if style.is_none() { |
| if let Ok(value) = input.try_parse(FontStyle::parse) { |
| style = Some(value); |
| count += 1; |
| continue; |
| } |
| } |
| if weight.is_none() { |
| if let Ok(value) = input.try_parse(FontWeight::parse) { |
| weight = Some(value); |
| count += 1; |
| continue; |
| } |
| } |
| if variant_caps.is_none() { |
| if let Ok(value) = input.try_parse(FontVariantCaps::parse_css2) { |
| variant_caps = Some(value); |
| count += 1; |
| continue; |
| } |
| } |
| |
| if stretch.is_none() { |
| if let Ok(value) = input.try_parse(FontStretchKeyword::parse) { |
| stretch = Some(FontStretch::Keyword(value)); |
| count += 1; |
| continue; |
| } |
| } |
| size = Some(FontSize::parse(input)?); |
| break; |
| } |
| |
| if count > 4 { |
| return Err(input.new_custom_error(ParserError::InvalidDeclaration)); |
| } |
| |
| let size = match size { |
| Some(s) => s, |
| None => return Err(input.new_custom_error(ParserError::InvalidDeclaration)), |
| }; |
| |
| let line_height = if input.try_parse(|input| input.expect_delim('/')).is_ok() { |
| Some(LineHeight::parse(input)?) |
| } else { |
| None |
| }; |
| |
| let family = input.parse_comma_separated(FontFamily::parse)?; |
| Ok(Font { |
| family, |
| size, |
| style: style.unwrap_or_default(), |
| weight: weight.unwrap_or_default(), |
| stretch: stretch.unwrap_or_default(), |
| line_height: line_height.unwrap_or_default(), |
| variant_caps: variant_caps.unwrap_or_default(), |
| }) |
| } |
| } |
| |
| impl<'i> ToCss for Font<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.style != FontStyle::default() { |
| self.style.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.variant_caps != FontVariantCaps::default() { |
| self.variant_caps.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.weight != FontWeight::default() { |
| self.weight.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.stretch != FontStretch::default() { |
| self.stretch.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| self.size.to_css(dest)?; |
| |
| if self.line_height != LineHeight::default() { |
| dest.delim('/', true)?; |
| self.line_height.to_css(dest)?; |
| } |
| |
| dest.write_char(' ')?; |
| |
| let len = self.family.len(); |
| for (idx, val) in self.family.iter().enumerate() { |
| val.to_css(dest)?; |
| if idx < len - 1 { |
| dest.delim(',', false)?; |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| property_bitflags! { |
| #[derive(Default, Debug)] |
| struct FontProperty: u8 { |
| const FontFamily = 1 << 0; |
| const FontSize = 1 << 1; |
| const FontStyle = 1 << 2; |
| const FontWeight = 1 << 3; |
| const FontStretch = 1 << 4; |
| const LineHeight = 1 << 5; |
| const FontVariantCaps = 1 << 6; |
| const Font = Self::FontFamily.bits() | Self::FontSize.bits() | Self::FontStyle.bits() | Self::FontWeight.bits() | Self::FontStretch.bits() | Self::LineHeight.bits() | Self::FontVariantCaps.bits(); |
| } |
| } |
| |
| #[derive(Default, Debug)] |
| pub(crate) struct FontHandler<'i> { |
| family: Option<Vec<FontFamily<'i>>>, |
| size: Option<FontSize>, |
| style: Option<FontStyle>, |
| weight: Option<FontWeight>, |
| stretch: Option<FontStretch>, |
| line_height: Option<LineHeight>, |
| variant_caps: Option<FontVariantCaps>, |
| flushed_properties: FontProperty, |
| has_any: bool, |
| } |
| |
| impl<'i> PropertyHandler<'i> for FontHandler<'i> { |
| fn handle_property( |
| &mut self, |
| property: &Property<'i>, |
| dest: &mut DeclarationList<'i>, |
| context: &mut PropertyHandlerContext<'i, '_>, |
| ) -> bool { |
| use Property::*; |
| |
| macro_rules! flush { |
| ($prop: ident, $val: expr) => {{ |
| if self.$prop.is_some() && self.$prop.as_ref().unwrap() != $val && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets)) { |
| self.flush(dest, context); |
| } |
| }}; |
| } |
| |
| macro_rules! property { |
| ($prop: ident, $val: ident) => {{ |
| flush!($prop, $val); |
| self.$prop = Some($val.clone()); |
| self.has_any = true; |
| }}; |
| } |
| |
| match property { |
| FontFamily(val) => property!(family, val), |
| FontSize(val) => property!(size, val), |
| FontStyle(val) => property!(style, val), |
| FontWeight(val) => property!(weight, val), |
| FontStretch(val) => property!(stretch, val), |
| FontVariantCaps(val) => property!(variant_caps, val), |
| LineHeight(val) => property!(line_height, val), |
| Font(val) => { |
| flush!(family, &val.family); |
| flush!(size, &val.size); |
| flush!(style, &val.style); |
| flush!(weight, &val.weight); |
| flush!(stretch, &val.stretch); |
| flush!(line_height, &val.line_height); |
| flush!(variant_caps, &val.variant_caps); |
| self.family = Some(val.family.clone()); |
| self.size = Some(val.size.clone()); |
| self.style = Some(val.style.clone()); |
| self.weight = Some(val.weight.clone()); |
| self.stretch = Some(val.stretch.clone()); |
| self.line_height = Some(val.line_height.clone()); |
| self.variant_caps = Some(val.variant_caps.clone()); |
| self.has_any = true; |
| // TODO: reset other properties |
| } |
| Unparsed(val) if is_font_property(&val.property_id) => { |
| self.flush(dest, context); |
| self |
| .flushed_properties |
| .insert(FontProperty::try_from(&val.property_id).unwrap()); |
| dest.push(property.clone()); |
| } |
| _ => return false, |
| } |
| |
| true |
| } |
| |
| fn finalize(&mut self, decls: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| self.flush(decls, context); |
| self.flushed_properties = FontProperty::empty(); |
| } |
| } |
| |
| impl<'i> FontHandler<'i> { |
| fn flush(&mut self, decls: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| if !self.has_any { |
| return; |
| } |
| |
| self.has_any = false; |
| |
| macro_rules! push { |
| ($prop: ident, $val: expr) => { |
| decls.push(Property::$prop($val)); |
| self.flushed_properties.insert(FontProperty::$prop); |
| }; |
| } |
| |
| let mut family = std::mem::take(&mut self.family); |
| if !self.flushed_properties.contains(FontProperty::FontFamily) { |
| family = compatible_font_family(family, !should_compile!(context.targets, FontFamilySystemUi)); |
| } |
| let size = std::mem::take(&mut self.size); |
| let style = std::mem::take(&mut self.style); |
| let weight = std::mem::take(&mut self.weight); |
| let stretch = std::mem::take(&mut self.stretch); |
| let line_height = std::mem::take(&mut self.line_height); |
| let variant_caps = std::mem::take(&mut self.variant_caps); |
| |
| if let Some(family) = &mut family { |
| if family.len() > 1 { |
| // Dedupe. |
| let mut seen = HashSet::new(); |
| family.retain(|f| seen.insert(f.clone())); |
| } |
| } |
| |
| if family.is_some() |
| && size.is_some() |
| && style.is_some() |
| && weight.is_some() |
| && stretch.is_some() |
| && line_height.is_some() |
| && variant_caps.is_some() |
| { |
| let caps = variant_caps.unwrap(); |
| push!( |
| Font, |
| Font { |
| family: family.unwrap(), |
| size: size.unwrap(), |
| style: style.unwrap(), |
| weight: weight.unwrap(), |
| stretch: stretch.unwrap(), |
| line_height: line_height.unwrap(), |
| variant_caps: if caps.is_css2() { |
| caps |
| } else { |
| FontVariantCaps::default() |
| }, |
| } |
| ); |
| |
| // The `font` property only accepts CSS 2.1 values for font-variant caps. |
| // If we have a CSS 3+ value, we need to add a separate property. |
| if !caps.is_css2() { |
| push!(FontVariantCaps, variant_caps.unwrap()); |
| } |
| } else { |
| if let Some(val) = family { |
| push!(FontFamily, val); |
| } |
| |
| if let Some(val) = size { |
| push!(FontSize, val); |
| } |
| |
| if let Some(val) = style { |
| push!(FontStyle, val); |
| } |
| |
| if let Some(val) = variant_caps { |
| push!(FontVariantCaps, val); |
| } |
| |
| if let Some(val) = weight { |
| push!(FontWeight, val); |
| } |
| |
| if let Some(val) = stretch { |
| push!(FontStretch, val); |
| } |
| |
| if let Some(val) = line_height { |
| push!(LineHeight, val); |
| } |
| } |
| } |
| } |
| |
| const SYSTEM_UI: FontFamily = FontFamily::Generic(GenericFontFamily::SystemUI); |
| |
| const DEFAULT_SYSTEM_FONTS: &[&str] = &[ |
| // #1: Supported as the '-apple-system' value (macOS, Safari >= 9.2 < 11, Firefox >= 43) |
| "-apple-system", |
| // #2: Supported as the 'BlinkMacSystemFont' value (macOS, Chrome < 56) |
| "BlinkMacSystemFont", |
| "Segoe UI", // Windows >= Vista |
| "Roboto", // Android >= 4 |
| "Noto Sans", // Plasma >= 5.5 |
| "Ubuntu", // Ubuntu >= 10.10 |
| "Cantarell", // GNOME >= 3 |
| "Helvetica Neue", |
| ]; |
| |
| /// [`system-ui`](https://www.w3.org/TR/css-fonts-4/#system-ui-def) is a special generic font family |
| /// It is platform dependent but if not supported by the target will simply be ignored |
| /// This list is an attempt at providing that support |
| #[inline] |
| fn compatible_font_family(mut family: Option<Vec<FontFamily>>, is_supported: bool) -> Option<Vec<FontFamily>> { |
| if is_supported { |
| return family; |
| } |
| |
| if let Some(families) = &mut family { |
| if let Some(position) = families.iter().position(|v| *v == SYSTEM_UI) { |
| families.splice( |
| (position + 1)..(position + 1), |
| DEFAULT_SYSTEM_FONTS |
| .iter() |
| .map(|name| FontFamily::FamilyName(FamilyName(CowArcStr::from(*name)))), |
| ); |
| } |
| } |
| |
| return family; |
| } |
| |
| #[inline] |
| fn is_font_property(property_id: &PropertyId) -> bool { |
| match property_id { |
| PropertyId::FontFamily |
| | PropertyId::FontSize |
| | PropertyId::FontStyle |
| | PropertyId::FontWeight |
| | PropertyId::FontStretch |
| | PropertyId::FontVariantCaps |
| | PropertyId::LineHeight |
| | PropertyId::Font => true, |
| _ => false, |
| } |
| } |