| //! The `@font-face` rule. |
| |
| use super::Location; |
| use crate::error::{ParserError, PrinterError}; |
| use crate::macros::enum_property; |
| use crate::printer::Printer; |
| use crate::properties::custom::CustomProperty; |
| use crate::properties::font::{FontFamily, FontStretch, FontStyle as FontStyleProperty, FontWeight}; |
| use crate::stylesheet::ParserOptions; |
| use crate::traits::{Parse, ToCss}; |
| use crate::values::angle::Angle; |
| use crate::values::size::Size2D; |
| use crate::values::string::CowArcStr; |
| use crate::values::url::Url; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use cssparser::*; |
| use std::fmt::Write; |
| |
| /// A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule. |
| #[derive(Debug, PartialEq, Clone)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub struct FontFaceRule<'i> { |
| /// Declarations in the `@font-face` rule. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| pub properties: Vec<FontFaceProperty<'i>>, |
| /// The location of the rule in the source file. |
| #[cfg_attr(feature = "visitor", skip_visit)] |
| pub loc: Location, |
| } |
| |
| /// A property within an `@font-face` rule. |
| /// |
| /// See [FontFaceRule](FontFaceRule). |
| #[derive(Debug, Clone, PartialEq)] |
| #[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(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum FontFaceProperty<'i> { |
| /// The `src` property. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| Source(Vec<Source<'i>>), |
| /// The `font-family` property. |
| FontFamily(FontFamily<'i>), |
| /// The `font-style` property. |
| FontStyle(FontStyle), |
| /// The `font-weight` property. |
| FontWeight(Size2D<FontWeight>), |
| /// The `font-stretch` property. |
| FontStretch(Size2D<FontStretch>), |
| /// The `unicode-range` property. |
| UnicodeRange(Vec<UnicodeRange>), |
| /// An unknown or unsupported property. |
| Custom(CustomProperty<'i>), |
| } |
| |
| /// A value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) |
| /// property in an `@font-face` rule. |
| #[derive(Debug, Clone, PartialEq)] |
| #[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(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum Source<'i> { |
| /// A `url()` with optional format metadata. |
| Url(UrlSource<'i>), |
| /// The `local()` function. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| Local(FontFamily<'i>), |
| } |
| |
| impl<'i> Parse<'i> for Source<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| match input.try_parse(UrlSource::parse) { |
| Ok(url) => return Ok(Source::Url(url)), |
| e @ Err(ParseError { |
| kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid), |
| .. |
| }) => { |
| return Err(e.err().unwrap()); |
| } |
| _ => {} |
| } |
| |
| input.expect_function_matching("local")?; |
| let local = input.parse_nested_block(FontFamily::parse)?; |
| Ok(Source::Local(local)) |
| } |
| } |
| |
| impl<'i> ToCss for Source<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| Source::Url(url) => url.to_css(dest), |
| Source::Local(local) => { |
| dest.write_str("local(")?; |
| local.to_css(dest)?; |
| dest.write_char(')') |
| } |
| } |
| } |
| } |
| |
| /// A `url()` value for the [src](https://drafts.csswg.org/css-fonts/#src-desc) |
| /// property in an `@font-face` rule. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub struct UrlSource<'i> { |
| /// The URL. |
| pub url: Url<'i>, |
| /// Optional `format()` function. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| pub format: Option<FontFormat<'i>>, |
| /// Optional `tech()` function. |
| pub tech: Vec<FontTechnology>, |
| } |
| |
| impl<'i> Parse<'i> for UrlSource<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let url = Url::parse(input)?; |
| |
| let format = if input.try_parse(|input| input.expect_function_matching("format")).is_ok() { |
| Some(input.parse_nested_block(FontFormat::parse)?) |
| } else { |
| None |
| }; |
| |
| let tech = if input.try_parse(|input| input.expect_function_matching("tech")).is_ok() { |
| input.parse_nested_block(Vec::<FontTechnology>::parse)? |
| } else { |
| vec![] |
| }; |
| |
| Ok(UrlSource { url, format, tech }) |
| } |
| } |
| |
| impl<'i> ToCss for UrlSource<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.url.to_css(dest)?; |
| if let Some(format) = &self.format { |
| dest.whitespace()?; |
| dest.write_str("format(")?; |
| format.to_css(dest)?; |
| dest.write_char(')')?; |
| } |
| |
| if !self.tech.is_empty() { |
| dest.whitespace()?; |
| dest.write_str("tech(")?; |
| self.tech.to_css(dest)?; |
| dest.write_char(')')?; |
| } |
| Ok(()) |
| } |
| } |
| |
| /// A font format keyword in the `format()` function of the the |
| /// [src](https://drafts.csswg.org/css-fonts/#src-desc) |
| /// property of an `@font-face` rule. |
| #[derive(Debug, Clone, PartialEq)] |
| #[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(tag = "type", content = "value", rename_all = "lowercase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum FontFormat<'i> { |
| /// [src](https://drafts.csswg.org/css-fonts/#font-format-definitions) |
| /// A WOFF 1.0 font. |
| WOFF, |
| /// A WOFF 2.0 font. |
| WOFF2, |
| /// A TrueType font. |
| TrueType, |
| /// An OpenType font. |
| OpenType, |
| /// An Embedded OpenType (.eot) font. |
| #[cfg_attr(feature = "serde", serde(rename = "embedded-opentype"))] |
| EmbeddedOpenType, |
| /// OpenType Collection. |
| Collection, |
| /// An SVG font. |
| SVG, |
| /// An unknown format. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| String(CowArcStr<'i>), |
| } |
| |
| impl<'i> Parse<'i> for FontFormat<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let s = input.expect_ident_or_string()?; |
| match_ignore_ascii_case! { &s, |
| "woff" => Ok(FontFormat::WOFF), |
| "woff2" => Ok(FontFormat::WOFF2), |
| "truetype" => Ok(FontFormat::TrueType), |
| "opentype" => Ok(FontFormat::OpenType), |
| "embedded-opentype" => Ok(FontFormat::EmbeddedOpenType), |
| "collection" => Ok(FontFormat::Collection), |
| "svg" => Ok(FontFormat::SVG), |
| _ => Ok(FontFormat::String(s.into())) |
| } |
| } |
| } |
| |
| impl<'i> ToCss for FontFormat<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| use FontFormat::*; |
| let s = match self { |
| WOFF => "woff", |
| WOFF2 => "woff2", |
| TrueType => "truetype", |
| OpenType => "opentype", |
| EmbeddedOpenType => "embedded-opentype", |
| Collection => "collection", |
| SVG => "svg", |
| String(s) => &s, |
| }; |
| // Browser support for keywords rather than strings is very limited. |
| // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src |
| serialize_string(&s, dest)?; |
| Ok(()) |
| } |
| } |
| |
| enum_property! { |
| /// A font format keyword in the `format()` function of the the |
| /// [src](https://drafts.csswg.org/css-fonts/#src-desc) |
| /// property of an `@font-face` rule. |
| pub enum FontTechnology { |
| /// A font features tech descriptor in the `tech()`function of the |
| /// [src](https://drafts.csswg.org/css-fonts/#font-features-tech-values) |
| /// property of an `@font-face` rule. |
| /// Supports OpenType Features. |
| /// https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist |
| "features-opentype": FeaturesOpentype, |
| /// Supports Apple Advanced Typography Font Features. |
| /// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM09/AppendixF.html |
| "features-aat": FeaturesAat, |
| /// Supports Graphite Table Format. |
| /// https://scripts.sil.org/cms/scripts/render_download.php?site_id=nrsi&format=file&media_id=GraphiteBinaryFormat_3_0&filename=GraphiteBinaryFormat_3_0.pdf |
| "features-graphite": FeaturesGraphite, |
| |
| /// A color font tech descriptor in the `tech()`function of the |
| /// [src](https://drafts.csswg.org/css-fonts/#src-desc) |
| /// property of an `@font-face` rule. |
| /// Supports the `COLR` v0 table. |
| "color-colrv0": ColorCOLRv0, |
| /// Supports the `COLR` v1 table. |
| "color-colrv1": ColorCOLRv1, |
| /// Supports the `SVG` table. |
| "color-svg": ColorSVG, |
| /// Supports the `sbix` table. |
| "color-sbix": ColorSbix, |
| /// Supports the `CBDT` table. |
| "color-cbdt": ColorCBDT, |
| |
| /// Supports Variations |
| /// The variations tech refers to the support of font variations |
| "variations": Variations, |
| /// Supports Palettes |
| /// The palettes tech refers to support for font palettes |
| "palettes": Palettes, |
| /// Supports Incremental |
| /// The incremental tech refers to client support for incremental font loading, using either the range-request or the patch-subset method |
| "incremental": Incremental, |
| } |
| } |
| |
| /// A contiguous range of Unicode code points. |
| /// |
| /// Cannot be empty. Can represent a single code point when start == end. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct UnicodeRange { |
| /// Inclusive start of the range. In [0, end]. |
| pub start: u32, |
| /// Inclusive end of the range. In [0, 0x10FFFF]. |
| pub end: u32, |
| } |
| |
| impl<'i> Parse<'i> for UnicodeRange { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let range = cssparser::UnicodeRange::parse(input)?; |
| Ok(UnicodeRange { |
| start: range.start, |
| end: range.end, |
| }) |
| } |
| } |
| |
| impl ToCss for UnicodeRange { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| // Attempt to optimize the range to use question mark syntax. |
| if self.start != self.end { |
| // Find the first hex digit that differs between the start and end values. |
| let mut shift = 24; |
| let mut mask = 0xf << shift; |
| while shift > 0 { |
| let c1 = self.start & mask; |
| let c2 = self.end & mask; |
| if c1 != c2 { |
| break; |
| } |
| |
| mask = mask >> 4; |
| shift -= 4; |
| } |
| |
| // Get the remainder of the value. This must be 0x0 to 0xf for the rest |
| // of the value to use the question mark syntax. |
| shift += 4; |
| let remainder_mask = (1 << shift) - 1; |
| let start_remainder = self.start & remainder_mask; |
| let end_remainder = self.end & remainder_mask; |
| |
| if start_remainder == 0 && end_remainder == remainder_mask { |
| let start = (self.start & !remainder_mask) >> shift; |
| if start != 0 { |
| write!(dest, "U+{:X}", start)?; |
| } else { |
| dest.write_str("U+")?; |
| } |
| |
| while shift > 0 { |
| dest.write_char('?')?; |
| shift -= 4; |
| } |
| |
| return Ok(()); |
| } |
| } |
| |
| write!(dest, "U+{:X}", self.start)?; |
| if self.end != self.start { |
| write!(dest, "-{:X}", self.end)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| /// A value for the [font-style](https://w3c.github.io/csswg-drafts/css-fonts/#descdef-font-face-font-style) descriptor in an `@font-face` rule. |
| #[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"))] Size2D<Angle>), |
| } |
| |
| impl Default for FontStyle { |
| fn default() -> FontStyle { |
| FontStyle::Normal |
| } |
| } |
| |
| impl FontStyle { |
| #[inline] |
| fn default_oblique_angle() -> Size2D<Angle> { |
| Size2D( |
| FontStyleProperty::default_oblique_angle(), |
| FontStyleProperty::default_oblique_angle(), |
| ) |
| } |
| } |
| |
| impl<'i> Parse<'i> for FontStyle { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| Ok(match FontStyleProperty::parse(input)? { |
| FontStyleProperty::Normal => FontStyle::Normal, |
| FontStyleProperty::Italic => FontStyle::Italic, |
| FontStyleProperty::Oblique(angle) => { |
| let second_angle = input.try_parse(Angle::parse).unwrap_or_else(|_| angle.clone()); |
| FontStyle::Oblique(Size2D(angle, second_angle)) |
| } |
| }) |
| } |
| } |
| |
| 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(()) |
| } |
| } |
| } |
| } |
| |
| pub(crate) struct FontFaceDeclarationParser; |
| |
| /// Parse a declaration within {} block: `color: blue` |
| impl<'i> cssparser::DeclarationParser<'i> for FontFaceDeclarationParser { |
| type Declaration = FontFaceProperty<'i>; |
| type Error = ParserError<'i>; |
| |
| fn parse_value<'t>( |
| &mut self, |
| name: CowRcStr<'i>, |
| input: &mut cssparser::Parser<'i, 't>, |
| ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> { |
| macro_rules! property { |
| ($property: ident, $type: ty) => { |
| if let Ok(c) = <$type>::parse(input) { |
| if input.expect_exhausted().is_ok() { |
| return Ok(FontFaceProperty::$property(c)); |
| } |
| } |
| }; |
| } |
| |
| let state = input.state(); |
| match_ignore_ascii_case! { &name, |
| "src" => { |
| if let Ok(sources) = input.parse_comma_separated(Source::parse) { |
| return Ok(FontFaceProperty::Source(sources)) |
| } |
| }, |
| "font-family" => property!(FontFamily, FontFamily), |
| "font-weight" => property!(FontWeight, Size2D<FontWeight>), |
| "font-style" => property!(FontStyle, FontStyle), |
| "font-stretch" => property!(FontStretch, Size2D<FontStretch>), |
| "unicode-range" => property!(UnicodeRange, Vec<UnicodeRange>), |
| _ => {} |
| } |
| |
| input.reset(&state); |
| return Ok(FontFaceProperty::Custom(CustomProperty::parse( |
| name.into(), |
| input, |
| &ParserOptions::default(), |
| )?)); |
| } |
| } |
| |
| /// Default methods reject all at rules. |
| impl<'i> AtRuleParser<'i> for FontFaceDeclarationParser { |
| type Prelude = (); |
| type AtRule = FontFaceProperty<'i>; |
| type Error = ParserError<'i>; |
| } |
| |
| impl<'i> QualifiedRuleParser<'i> for FontFaceDeclarationParser { |
| type Prelude = (); |
| type QualifiedRule = FontFaceProperty<'i>; |
| type Error = ParserError<'i>; |
| } |
| |
| impl<'i> RuleBodyItemParser<'i, FontFaceProperty<'i>, ParserError<'i>> for FontFaceDeclarationParser { |
| fn parse_qualified(&self) -> bool { |
| false |
| } |
| |
| fn parse_declarations(&self) -> bool { |
| true |
| } |
| } |
| |
| impl<'i> ToCss for FontFaceRule<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| #[cfg(feature = "sourcemap")] |
| dest.add_mapping(self.loc); |
| dest.write_str("@font-face")?; |
| dest.whitespace()?; |
| dest.write_char('{')?; |
| dest.indent(); |
| let len = self.properties.len(); |
| for (i, prop) in self.properties.iter().enumerate() { |
| dest.newline()?; |
| prop.to_css(dest)?; |
| if i != len - 1 || !dest.minify { |
| dest.write_char(';')?; |
| } |
| } |
| dest.dedent(); |
| dest.newline()?; |
| dest.write_char('}') |
| } |
| } |
| |
| impl<'i> ToCss for FontFaceProperty<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| use FontFaceProperty::*; |
| macro_rules! property { |
| ($prop: literal, $value: expr) => {{ |
| dest.write_str($prop)?; |
| dest.delim(':', false)?; |
| $value.to_css(dest) |
| }}; |
| ($prop: literal, $value: expr, $multi: expr) => {{ |
| dest.write_str($prop)?; |
| dest.delim(':', false)?; |
| let len = $value.len(); |
| for (idx, val) in $value.iter().enumerate() { |
| val.to_css(dest)?; |
| if idx < len - 1 { |
| dest.delim(',', false)?; |
| } |
| } |
| Ok(()) |
| }}; |
| } |
| |
| match self { |
| Source(value) => property!("src", value, true), |
| FontFamily(value) => property!("font-family", value), |
| FontStyle(value) => property!("font-style", value), |
| FontWeight(value) => property!("font-weight", value), |
| FontStretch(value) => property!("font-stretch", value), |
| UnicodeRange(value) => property!("unicode-range", value), |
| Custom(custom) => { |
| dest.write_str(custom.name.as_ref())?; |
| dest.delim(':', false)?; |
| custom.value.to_css(dest, true) |
| } |
| } |
| } |
| } |