use std::fmt;
use std::string::ToString;
use cssparser::{Parser, match_ignore_ascii_case};
use parley::{
FontFamily as ParleyFontFamily, FontFamilyName, GenericFamily, fontique::QueryFamily,
};
use crate::layout::style::{
CssSyntaxKind, CssToken, FromCss, MakeComputed, ParseResult, ToCss, properties::write_css_string,
tw::TailwindPropertyParser,
};
#[derive(Debug, Clone, PartialEq)]
pub struct FontFamily(Box<[FontFamilyToken]>);
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum FontFamilyToken {
Owned(String),
Generic(GenericFamily),
}
impl MakeComputed for FontFamily {}
impl FontFamily {
pub(crate) fn query_families(&self) -> impl Iterator<Item = QueryFamily<'_>> + Clone {
self.0.iter().map(|token| match token {
FontFamilyToken::Owned(name) => QueryFamily::Named(name.as_str()),
FontFamilyToken::Generic(generic) => QueryFamily::Generic(*generic),
})
}
}
impl<'i> FromCss<'i> for FontFamilyToken {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
if let Ok(name) = input.try_parse(|input| input.expect_string().map(ToString::to_string)) {
return Ok(Self::Owned(name));
}
let mut family_name = input.expect_ident()?.to_string();
while let Ok(ident) = input.try_parse(Parser::expect_ident_cloned) {
family_name.push(' ');
family_name.push_str(&ident);
}
if let Some(generic) = GenericFamily::parse(&family_name) {
return Ok(Self::Generic(generic));
}
Ok(Self::Owned(family_name))
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Syntax(CssSyntaxKind::FamilyName),
CssToken::Syntax(CssSyntaxKind::GenericName),
];
}
impl<'i> FromCss<'i> for FontFamily {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
let list = input.parse_comma_separated(FontFamilyToken::from_css)?;
Ok(Self(list.into_boxed_slice()))
}
const VALID_TOKENS: &'static [CssToken] = FontFamilyToken::VALID_TOKENS;
}
impl TailwindPropertyParser for FontFamily {
fn parse_tw(token: &str) -> Option<Self> {
match_ignore_ascii_case! {token,
"sans" => Some(GenericFamily::SansSerif.into()),
"serif" => Some(GenericFamily::Serif.into()),
"mono" => Some(GenericFamily::Monospace.into()),
_ => None,
}
}
}
impl Default for FontFamily {
fn default() -> Self {
GenericFamily::SansSerif.into()
}
}
impl<'a> From<FontFamily> for ParleyFontFamily<'a> {
fn from(family: FontFamily) -> Self {
ParleyFontFamily::List(
family
.0
.into_iter()
.map(|token| match token {
FontFamilyToken::Owned(name) => FontFamilyName::Named(name.into()),
FontFamilyToken::Generic(generic) => FontFamilyName::Generic(generic),
})
.collect(),
)
}
}
impl<'a> From<&'a FontFamily> for ParleyFontFamily<'a> {
fn from(family: &'a FontFamily) -> Self {
ParleyFontFamily::List(
family
.0
.iter()
.map(|token| match token {
FontFamilyToken::Owned(name) => FontFamilyName::Named(name.as_str().into()),
FontFamilyToken::Generic(generic) => FontFamilyName::Generic(*generic),
})
.collect(),
)
}
}
impl From<GenericFamily> for FontFamily {
fn from(generic: GenericFamily) -> Self {
Self(Box::new([FontFamilyToken::Generic(generic)]))
}
}
impl ToCss for FontFamilyToken {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::Owned(name) => {
let needs_quoting = name.contains(' ')
|| name.contains('"')
|| name.contains('\\')
|| name.chars().next().is_some_and(|c| c.is_ascii_digit());
if needs_quoting {
write_css_string(dest, name)
} else {
dest.write_str(name)
}
}
Self::Generic(generic) => dest.write_str(match generic {
GenericFamily::Serif => "serif",
GenericFamily::SansSerif => "sans-serif",
GenericFamily::Monospace => "monospace",
GenericFamily::Cursive => "cursive",
GenericFamily::Fantasy => "fantasy",
GenericFamily::SystemUi => "system-ui",
GenericFamily::Emoji => "emoji",
_ => "sans-serif",
}),
}
}
}
impl ToCss for FontFamily {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
let mut first = true;
for token in self.0.iter() {
if !first {
dest.write_str(", ")?;
}
first = false;
token.to_css(dest)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use parley::GenericFamily;
use super::{FontFamily, FontFamilyToken};
use crate::layout::style::{FromCss, tw::TailwindPropertyParser};
#[test]
fn parses_single_generic_family() {
assert_eq!(
FontFamily::from_str("serif"),
Ok(FontFamily(Box::new([FontFamilyToken::Generic(
GenericFamily::Serif,
)])))
);
}
#[test]
fn parses_fallback_family_list() {
assert_eq!(
FontFamily::from_str("\"Inter\", Arial, serif"),
Ok(FontFamily(Box::new([
FontFamilyToken::Owned(String::from("Inter")),
FontFamilyToken::Owned(String::from("Arial")),
FontFamilyToken::Generic(GenericFamily::Serif),
])))
);
}
#[test]
fn parses_unquoted_multi_word_family_name() {
assert_eq!(
FontFamily::from_str("Noto Sans TC"),
Ok(FontFamily(Box::new([FontFamilyToken::Owned(
"Noto Sans TC".to_string()
)])))
);
}
#[test]
fn parses_tailwind_aliases() {
assert_eq!(
FontFamily::parse_tw("sans"),
Some(FontFamily(Box::new([FontFamilyToken::Generic(
GenericFamily::SansSerif,
)])))
);
assert_eq!(
FontFamily::parse_tw("mono"),
Some(FontFamily(Box::new([FontFamilyToken::Generic(
GenericFamily::Monospace,
)])))
);
assert_eq!(FontFamily::parse_tw("display"), None);
}
}