lightningcss/rules/
font_face.rs

1//! The `@font-face` rule.
2
3use super::Location;
4use crate::error::{ParserError, PrinterError};
5use crate::macros::enum_property;
6use crate::printer::Printer;
7use crate::properties::custom::CustomProperty;
8use crate::properties::font::{FontFamily, FontStretch, FontStyle as FontStyleProperty, FontWeight};
9use crate::stylesheet::ParserOptions;
10use crate::traits::{Parse, ToCss};
11use crate::values::angle::Angle;
12use crate::values::size::Size2D;
13use crate::values::string::CowArcStr;
14use crate::values::url::Url;
15#[cfg(feature = "visitor")]
16use crate::visitor::Visit;
17use cssparser::*;
18use std::fmt::Write;
19
20/// A [@font-face](https://drafts.csswg.org/css-fonts/#font-face-rule) rule.
21#[derive(Debug, PartialEq, Clone)]
22#[cfg_attr(feature = "visitor", derive(Visit))]
23#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
26pub struct FontFaceRule<'i> {
27  /// Declarations in the `@font-face` rule.
28  #[cfg_attr(feature = "serde", serde(borrow))]
29  pub properties: Vec<FontFaceProperty<'i>>,
30  /// The location of the rule in the source file.
31  #[cfg_attr(feature = "visitor", skip_visit)]
32  pub loc: Location,
33}
34
35/// A property within an `@font-face` rule.
36///
37/// See [FontFaceRule](FontFaceRule).
38#[derive(Debug, Clone, PartialEq)]
39#[cfg_attr(feature = "visitor", derive(Visit))]
40#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
41#[cfg_attr(
42  feature = "serde",
43  derive(serde::Serialize, serde::Deserialize),
44  serde(tag = "type", content = "value", rename_all = "kebab-case")
45)]
46#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
47pub enum FontFaceProperty<'i> {
48  /// The `src` property.
49  #[cfg_attr(feature = "serde", serde(borrow))]
50  Source(Vec<Source<'i>>),
51  /// The `font-family` property.
52  FontFamily(FontFamily<'i>),
53  /// The `font-style` property.
54  FontStyle(FontStyle),
55  /// The `font-weight` property.
56  FontWeight(Size2D<FontWeight>),
57  /// The `font-stretch` property.
58  FontStretch(Size2D<FontStretch>),
59  /// The `unicode-range` property.
60  UnicodeRange(Vec<UnicodeRange>),
61  /// An unknown or unsupported property.
62  Custom(CustomProperty<'i>),
63}
64
65/// A value for the [src](https://drafts.csswg.org/css-fonts/#src-desc)
66/// property in an `@font-face` rule.
67#[derive(Debug, Clone, PartialEq)]
68#[cfg_attr(feature = "visitor", derive(Visit))]
69#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
70#[cfg_attr(
71  feature = "serde",
72  derive(serde::Serialize, serde::Deserialize),
73  serde(tag = "type", content = "value", rename_all = "kebab-case")
74)]
75#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
76pub enum Source<'i> {
77  /// A `url()` with optional format metadata.
78  Url(UrlSource<'i>),
79  /// The `local()` function.
80  #[cfg_attr(feature = "serde", serde(borrow))]
81  Local(FontFamily<'i>),
82}
83
84impl<'i> Parse<'i> for Source<'i> {
85  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
86    match input.try_parse(UrlSource::parse) {
87      Ok(url) => return Ok(Source::Url(url)),
88      e @ Err(ParseError {
89        kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid),
90        ..
91      }) => {
92        return Err(e.err().unwrap());
93      }
94      _ => {}
95    }
96
97    input.expect_function_matching("local")?;
98    let local = input.parse_nested_block(FontFamily::parse)?;
99    Ok(Source::Local(local))
100  }
101}
102
103impl<'i> ToCss for Source<'i> {
104  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
105  where
106    W: std::fmt::Write,
107  {
108    match self {
109      Source::Url(url) => url.to_css(dest),
110      Source::Local(local) => {
111        dest.write_str("local(")?;
112        local.to_css(dest)?;
113        dest.write_char(')')
114      }
115    }
116  }
117}
118
119/// A `url()` value for the [src](https://drafts.csswg.org/css-fonts/#src-desc)
120/// property in an `@font-face` rule.
121#[derive(Debug, Clone, PartialEq)]
122#[cfg_attr(feature = "visitor", derive(Visit))]
123#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
126pub struct UrlSource<'i> {
127  /// The URL.
128  pub url: Url<'i>,
129  /// Optional `format()` function.
130  #[cfg_attr(feature = "serde", serde(borrow))]
131  pub format: Option<FontFormat<'i>>,
132  /// Optional `tech()` function.
133  pub tech: Vec<FontTechnology>,
134}
135
136impl<'i> Parse<'i> for UrlSource<'i> {
137  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
138    let url = Url::parse(input)?;
139
140    let format = if input.try_parse(|input| input.expect_function_matching("format")).is_ok() {
141      Some(input.parse_nested_block(FontFormat::parse)?)
142    } else {
143      None
144    };
145
146    let tech = if input.try_parse(|input| input.expect_function_matching("tech")).is_ok() {
147      input.parse_nested_block(Vec::<FontTechnology>::parse)?
148    } else {
149      vec![]
150    };
151
152    Ok(UrlSource { url, format, tech })
153  }
154}
155
156impl<'i> ToCss for UrlSource<'i> {
157  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
158  where
159    W: std::fmt::Write,
160  {
161    self.url.to_css(dest)?;
162    if let Some(format) = &self.format {
163      dest.whitespace()?;
164      dest.write_str("format(")?;
165      format.to_css(dest)?;
166      dest.write_char(')')?;
167    }
168
169    if !self.tech.is_empty() {
170      dest.whitespace()?;
171      dest.write_str("tech(")?;
172      self.tech.to_css(dest)?;
173      dest.write_char(')')?;
174    }
175    Ok(())
176  }
177}
178
179/// A font format keyword in the `format()` function of the the
180/// [src](https://drafts.csswg.org/css-fonts/#src-desc)
181/// property of an `@font-face` rule.
182#[derive(Debug, Clone, PartialEq)]
183#[cfg_attr(feature = "visitor", derive(Visit))]
184#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
185#[cfg_attr(
186  feature = "serde",
187  derive(serde::Serialize, serde::Deserialize),
188  serde(tag = "type", content = "value", rename_all = "lowercase")
189)]
190#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
191pub enum FontFormat<'i> {
192  /// [src](https://drafts.csswg.org/css-fonts/#font-format-definitions)
193  /// A WOFF 1.0 font.
194  WOFF,
195  /// A WOFF 2.0 font.
196  WOFF2,
197  /// A TrueType font.
198  TrueType,
199  /// An OpenType font.
200  OpenType,
201  /// An Embedded OpenType (.eot) font.
202  #[cfg_attr(feature = "serde", serde(rename = "embedded-opentype"))]
203  EmbeddedOpenType,
204  /// OpenType Collection.
205  Collection,
206  /// An SVG font.
207  SVG,
208  /// An unknown format.
209  #[cfg_attr(feature = "serde", serde(borrow))]
210  String(CowArcStr<'i>),
211}
212
213impl<'i> Parse<'i> for FontFormat<'i> {
214  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
215    let s = input.expect_ident_or_string()?;
216    match_ignore_ascii_case! { &s,
217      "woff" => Ok(FontFormat::WOFF),
218      "woff2" => Ok(FontFormat::WOFF2),
219      "truetype" => Ok(FontFormat::TrueType),
220      "opentype" => Ok(FontFormat::OpenType),
221      "embedded-opentype" => Ok(FontFormat::EmbeddedOpenType),
222      "collection" => Ok(FontFormat::Collection),
223      "svg" => Ok(FontFormat::SVG),
224      _ => Ok(FontFormat::String(s.into()))
225    }
226  }
227}
228
229impl<'i> ToCss for FontFormat<'i> {
230  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
231  where
232    W: std::fmt::Write,
233  {
234    use FontFormat::*;
235    let s = match self {
236      WOFF => "woff",
237      WOFF2 => "woff2",
238      TrueType => "truetype",
239      OpenType => "opentype",
240      EmbeddedOpenType => "embedded-opentype",
241      Collection => "collection",
242      SVG => "svg",
243      String(s) => &s,
244    };
245    // Browser support for keywords rather than strings is very limited.
246    // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src
247    serialize_string(&s, dest)?;
248    Ok(())
249  }
250}
251
252enum_property! {
253  /// A font format keyword in the `format()` function of the the
254  /// [src](https://drafts.csswg.org/css-fonts/#src-desc)
255  /// property of an `@font-face` rule.
256  pub enum FontTechnology {
257    /// A font features tech descriptor in the `tech()`function of the
258    /// [src](https://drafts.csswg.org/css-fonts/#font-features-tech-values)
259    /// property of an `@font-face` rule.
260    /// Supports OpenType Features.
261    /// https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
262    "features-opentype": FeaturesOpentype,
263    /// Supports Apple Advanced Typography Font Features.
264    /// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM09/AppendixF.html
265    "features-aat": FeaturesAat,
266    /// Supports Graphite Table Format.
267    /// https://scripts.sil.org/cms/scripts/render_download.php?site_id=nrsi&format=file&media_id=GraphiteBinaryFormat_3_0&filename=GraphiteBinaryFormat_3_0.pdf
268    "features-graphite": FeaturesGraphite,
269
270    /// A color font tech descriptor in the `tech()`function of the
271    /// [src](https://drafts.csswg.org/css-fonts/#src-desc)
272    /// property of an `@font-face` rule.
273    /// Supports the `COLR` v0 table.
274    "color-colrv0": ColorCOLRv0,
275    /// Supports the `COLR` v1 table.
276    "color-colrv1": ColorCOLRv1,
277    /// Supports the `SVG` table.
278    "color-svg": ColorSVG,
279    /// Supports the `sbix` table.
280    "color-sbix": ColorSbix,
281    /// Supports the `CBDT` table.
282    "color-cbdt": ColorCBDT,
283
284    /// Supports Variations
285    /// The variations tech refers to the support of font variations
286    "variations": Variations,
287    /// Supports Palettes
288    /// The palettes tech refers to support for font palettes
289    "palettes": Palettes,
290    /// Supports Incremental
291    /// The incremental tech refers to client support for incremental font loading, using either the range-request or the patch-subset method
292    "incremental": Incremental,
293  }
294}
295
296/// A contiguous range of Unicode code points.
297///
298/// Cannot be empty. Can represent a single code point when start == end.
299#[derive(Debug, Clone, PartialEq)]
300#[cfg_attr(feature = "visitor", derive(Visit))]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
303#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
304pub struct UnicodeRange {
305  /// Inclusive start of the range. In [0, end].
306  pub start: u32,
307  /// Inclusive end of the range. In [0, 0x10FFFF].
308  pub end: u32,
309}
310
311impl<'i> Parse<'i> for UnicodeRange {
312  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
313    let range = cssparser::UnicodeRange::parse(input)?;
314    Ok(UnicodeRange {
315      start: range.start,
316      end: range.end,
317    })
318  }
319}
320
321impl ToCss for UnicodeRange {
322  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
323  where
324    W: std::fmt::Write,
325  {
326    // Attempt to optimize the range to use question mark syntax.
327    if self.start != self.end {
328      // Find the first hex digit that differs between the start and end values.
329      let mut shift = 24;
330      let mut mask = 0xf << shift;
331      while shift > 0 {
332        let c1 = self.start & mask;
333        let c2 = self.end & mask;
334        if c1 != c2 {
335          break;
336        }
337
338        mask = mask >> 4;
339        shift -= 4;
340      }
341
342      // Get the remainder of the value. This must be 0x0 to 0xf for the rest
343      // of the value to use the question mark syntax.
344      shift += 4;
345      let remainder_mask = (1 << shift) - 1;
346      let start_remainder = self.start & remainder_mask;
347      let end_remainder = self.end & remainder_mask;
348
349      if start_remainder == 0 && end_remainder == remainder_mask {
350        let start = (self.start & !remainder_mask) >> shift;
351        if start != 0 {
352          write!(dest, "U+{:X}", start)?;
353        } else {
354          dest.write_str("U+")?;
355        }
356
357        while shift > 0 {
358          dest.write_char('?')?;
359          shift -= 4;
360        }
361
362        return Ok(());
363      }
364    }
365
366    write!(dest, "U+{:X}", self.start)?;
367    if self.end != self.start {
368      write!(dest, "-{:X}", self.end)?;
369    }
370    Ok(())
371  }
372}
373
374/// 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.
375#[derive(Debug, Clone, PartialEq)]
376#[cfg_attr(feature = "visitor", derive(Visit))]
377#[cfg_attr(
378  feature = "serde",
379  derive(serde::Serialize, serde::Deserialize),
380  serde(tag = "type", content = "value", rename_all = "kebab-case")
381)]
382#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
383#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
384pub enum FontStyle {
385  /// Normal font style.
386  Normal,
387  /// Italic font style.
388  Italic,
389  /// Oblique font style, with a custom angle.
390  Oblique(#[cfg_attr(feature = "serde", serde(default = "FontStyle::default_oblique_angle"))] Size2D<Angle>),
391}
392
393impl Default for FontStyle {
394  fn default() -> FontStyle {
395    FontStyle::Normal
396  }
397}
398
399impl FontStyle {
400  #[inline]
401  fn default_oblique_angle() -> Size2D<Angle> {
402    Size2D(
403      FontStyleProperty::default_oblique_angle(),
404      FontStyleProperty::default_oblique_angle(),
405    )
406  }
407}
408
409impl<'i> Parse<'i> for FontStyle {
410  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
411    Ok(match FontStyleProperty::parse(input)? {
412      FontStyleProperty::Normal => FontStyle::Normal,
413      FontStyleProperty::Italic => FontStyle::Italic,
414      FontStyleProperty::Oblique(angle) => {
415        let second_angle = input.try_parse(Angle::parse).unwrap_or_else(|_| angle.clone());
416        FontStyle::Oblique(Size2D(angle, second_angle))
417      }
418    })
419  }
420}
421
422impl ToCss for FontStyle {
423  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
424  where
425    W: std::fmt::Write,
426  {
427    match self {
428      FontStyle::Normal => dest.write_str("normal"),
429      FontStyle::Italic => dest.write_str("italic"),
430      FontStyle::Oblique(angle) => {
431        dest.write_str("oblique")?;
432        if *angle != FontStyle::default_oblique_angle() {
433          dest.write_char(' ')?;
434          angle.to_css(dest)?;
435        }
436        Ok(())
437      }
438    }
439  }
440}
441
442pub(crate) struct FontFaceDeclarationParser;
443
444/// Parse a declaration within {} block: `color: blue`
445impl<'i> cssparser::DeclarationParser<'i> for FontFaceDeclarationParser {
446  type Declaration = FontFaceProperty<'i>;
447  type Error = ParserError<'i>;
448
449  fn parse_value<'t>(
450    &mut self,
451    name: CowRcStr<'i>,
452    input: &mut cssparser::Parser<'i, 't>,
453  ) -> Result<Self::Declaration, cssparser::ParseError<'i, Self::Error>> {
454    macro_rules! property {
455      ($property: ident, $type: ty) => {
456        if let Ok(c) = <$type>::parse(input) {
457          if input.expect_exhausted().is_ok() {
458            return Ok(FontFaceProperty::$property(c));
459          }
460        }
461      };
462    }
463
464    let state = input.state();
465    match_ignore_ascii_case! { &name,
466      "src" => {
467        if let Ok(sources) = input.parse_comma_separated(Source::parse) {
468          return Ok(FontFaceProperty::Source(sources))
469        }
470      },
471      "font-family" => property!(FontFamily, FontFamily),
472      "font-weight" => property!(FontWeight, Size2D<FontWeight>),
473      "font-style" => property!(FontStyle, FontStyle),
474      "font-stretch" => property!(FontStretch, Size2D<FontStretch>),
475      "unicode-range" => property!(UnicodeRange, Vec<UnicodeRange>),
476      _ => {}
477    }
478
479    input.reset(&state);
480    return Ok(FontFaceProperty::Custom(CustomProperty::parse(
481      name.into(),
482      input,
483      &ParserOptions::default(),
484    )?));
485  }
486}
487
488/// Default methods reject all at rules.
489impl<'i> AtRuleParser<'i> for FontFaceDeclarationParser {
490  type Prelude = ();
491  type AtRule = FontFaceProperty<'i>;
492  type Error = ParserError<'i>;
493}
494
495impl<'i> QualifiedRuleParser<'i> for FontFaceDeclarationParser {
496  type Prelude = ();
497  type QualifiedRule = FontFaceProperty<'i>;
498  type Error = ParserError<'i>;
499}
500
501impl<'i> RuleBodyItemParser<'i, FontFaceProperty<'i>, ParserError<'i>> for FontFaceDeclarationParser {
502  fn parse_qualified(&self) -> bool {
503    false
504  }
505
506  fn parse_declarations(&self) -> bool {
507    true
508  }
509}
510
511impl<'i> ToCss for FontFaceRule<'i> {
512  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
513  where
514    W: std::fmt::Write,
515  {
516    #[cfg(feature = "sourcemap")]
517    dest.add_mapping(self.loc);
518    dest.write_str("@font-face")?;
519    dest.whitespace()?;
520    dest.write_char('{')?;
521    dest.indent();
522    let len = self.properties.len();
523    for (i, prop) in self.properties.iter().enumerate() {
524      dest.newline()?;
525      prop.to_css(dest)?;
526      if i != len - 1 || !dest.minify {
527        dest.write_char(';')?;
528      }
529    }
530    dest.dedent();
531    dest.newline()?;
532    dest.write_char('}')
533  }
534}
535
536impl<'i> ToCss for FontFaceProperty<'i> {
537  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
538  where
539    W: std::fmt::Write,
540  {
541    use FontFaceProperty::*;
542    macro_rules! property {
543      ($prop: literal, $value: expr) => {{
544        dest.write_str($prop)?;
545        dest.delim(':', false)?;
546        $value.to_css(dest)
547      }};
548      ($prop: literal, $value: expr, $multi: expr) => {{
549        dest.write_str($prop)?;
550        dest.delim(':', false)?;
551        let len = $value.len();
552        for (idx, val) in $value.iter().enumerate() {
553          val.to_css(dest)?;
554          if idx < len - 1 {
555            dest.delim(',', false)?;
556          }
557        }
558        Ok(())
559      }};
560    }
561
562    match self {
563      Source(value) => property!("src", value, true),
564      FontFamily(value) => property!("font-family", value),
565      FontStyle(value) => property!("font-style", value),
566      FontWeight(value) => property!("font-weight", value),
567      FontStretch(value) => property!("font-stretch", value),
568      UnicodeRange(value) => property!("unicode-range", value),
569      Custom(custom) => {
570        dest.write_str(custom.name.as_ref())?;
571        dest.delim(':', false)?;
572        custom.value.to_css(dest, true)
573      }
574    }
575  }
576}