Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add font family parser #20

Merged
merged 27 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/aspect_ratio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl std::str::FromStr for AspectRatio {
}

let start = s.pos();
let align = s.consume_ident();
let align = s.consume_ascii_ident();
let align = match align {
"none" => Align::None,
"xMinYMin" => Align::XMinYMin,
Expand All @@ -74,7 +74,7 @@ impl std::str::FromStr for AspectRatio {
let mut slice = false;
if !s.at_end() {
let start = s.pos();
let v = s.consume_ident();
let v = s.consume_ascii_ident();
match v {
"meet" => {}
"slice" => slice = true,
Expand Down
2 changes: 1 addition & 1 deletion src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ impl<'a> Stream<'a> {
}
} else {
// TODO: remove allocation
let name = self.consume_ident().to_ascii_lowercase();
let name = self.consume_ascii_ident().to_ascii_lowercase();
if name == "rgb" || name == "rgba" {
self.consume_byte(b'(')?;

Expand Down
12 changes: 11 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// List of all errors.
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
/// An input data ended earlier than expected.
///
Expand All @@ -18,6 +18,13 @@ pub enum Error {
/// then we will get `InvalidNumber`, because at least some data is valid.
InvalidValue,

/// An invalid ident.
///
/// CSS idents have certain rules with regard to the characters they may contain.
/// For example, they may not start with a number. If an invalid ident is encountered,
/// this error will be returned.
InvalidIdent,

/// An invalid/unexpected character.
///
/// The first byte is an actual one, others - expected.
Expand Down Expand Up @@ -48,6 +55,9 @@ impl std::fmt::Display for Error {
Error::InvalidValue => {
write!(f, "invalid value")
}
Error::InvalidIdent => {
write!(f, "invalid ident")
}
Error::InvalidChar(ref chars, pos) => {
// Vec<u8> -> Vec<String>
let list: Vec<String> = chars
Expand Down
2 changes: 1 addition & 1 deletion src/filter_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl<'a> FilterValueListParser<'a> {
let s = &mut self.stream;

let start = s.pos();
let name = s.consume_ident();
let name = s.consume_ascii_ident();
s.skip_spaces();
s.consume_byte(b'(')?;
s.skip_spaces();
Expand Down
171 changes: 171 additions & 0 deletions src/font.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use crate::stream::Stream;
use crate::Error;
use std::fmt::Display;

/// Parses a list of font families and generic families from a string.
pub fn parse_font_families(text: &str) -> Result<Vec<FontFamily>, Error> {
let mut s = Stream::from(text);
let font_families = s.parse_font_families()?;

s.skip_spaces();
if !s.at_end() {
return Err(Error::UnexpectedData(s.calc_char_pos()));
}

Ok(font_families)
}

/// A type of font family.
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub enum FontFamily {
LaurenzV marked this conversation as resolved.
Show resolved Hide resolved
/// A serif font.
Serif,
/// A sans-serif font.
SansSerif,
/// A cursive font.
Cursive,
/// A fantasy font.
Fantasy,
/// A monospace font.
Monospace,
/// A custom named font.
Named(String),
}

impl Display for FontFamily {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
FontFamily::Monospace => "monospace".to_string(),
FontFamily::Serif => "serif".to_string(),
FontFamily::SansSerif => "sans-serif".to_string(),
FontFamily::Cursive => "cursive".to_string(),
FontFamily::Fantasy => "fantasy".to_string(),
FontFamily::Named(s) => format!("\"{}\"", s),
};
write!(f, "{}", str)
}
}

impl<'a> Stream<'a> {
pub fn parse_font_families(&mut self) -> Result<Vec<FontFamily>, Error> {
let mut families = vec![];

while !self.at_end() {
self.skip_spaces();

let family = {
let ch = self.curr_byte()?;
if ch == b'\'' || ch == b'\"' {
let res = self.parse_quoted_string()?;
FontFamily::Named(res.to_string())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return &str?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I should have it take a Cow? because I also need to be able to pass an owned string (see below).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? You're never modifying the input string.

Copy link
Contributor Author

@LaurenzV LaurenzV Feb 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, here, basically when the font name is given as a list of idents instead of a quoted string:

let joined = idents.join(" ");

// TODO: No CSS keyword must be matched as a family name...
match joined.as_str() {
    "serif" => FontFamily::Serif,
    "sans-serif" => FontFamily::SansSerif,
    "cursive" => FontFamily::Cursive,
    "fantasy" => FontFamily::Fantasy,
    "monospace" => FontFamily::Monospace,
    _ => FontFamily::Named(joined),
}

The spec requires us to parse multiple spaces as a single space (which should again probably be done at the CSS parsing level, but since it's relatively straight-forward I just implemented it here).

} else {
let mut idents = vec![];

while let Some(c) = self.chars().next() {
if c != ',' {
idents.push(self.parse_ident()?.to_string());
self.skip_spaces();
} else {
break;
}
}

let joined = idents.join(" ");

// TODO: No CSS keyword must be matched as a family name...
match joined.as_str() {
"serif" => FontFamily::Serif,
"sans-serif" => FontFamily::SansSerif,
"cursive" => FontFamily::Cursive,
"fantasy" => FontFamily::Fantasy,
"monospace" => FontFamily::Monospace,
_ => FontFamily::Named(joined),
}
}
};

families.push(family);

if let Ok(b) = self.curr_byte() {
if b == b',' {
self.advance(1);
} else {
break;
}
}
}

let families = families
.into_iter()
.filter(|f| match f {
FontFamily::Named(s) => !s.is_empty(),
_ => true,
})
.collect();

Ok(families)
}
}

#[rustfmt::skip]
#[cfg(test)]
mod tests {
use super::*;

macro_rules! test {
($name:ident, $text:expr, $result:expr) => (
#[test]
fn $name() {
assert_eq!(parse_font_families($text).unwrap(), $result);
}
)
}

macro_rules! named {
($text:expr) => (
FontFamily::Named($text.to_string())
)
}

const SERIF: FontFamily = FontFamily::Serif;
const SANS_SERIF: FontFamily = FontFamily::SansSerif;
const FANTASY: FontFamily = FontFamily::Fantasy;
const MONOSPACE: FontFamily = FontFamily::Monospace;
const CURSIVE: FontFamily = FontFamily::Cursive;

test!(font_family_1, "Times New Roman", vec![named!("Times New Roman")]);
test!(font_family_2, "serif", vec![SERIF]);
test!(font_family_3, "sans-serif", vec![SANS_SERIF]);
test!(font_family_4, "cursive", vec![CURSIVE]);
test!(font_family_5, "fantasy", vec![FANTASY]);
test!(font_family_6, "monospace", vec![MONOSPACE]);
test!(font_family_7, "'Times New Roman'", vec![named!("Times New Roman")]);
test!(font_family_8, "'Times New Roman', sans-serif", vec![named!("Times New Roman"), SANS_SERIF]);
test!(font_family_9, "'Times New Roman', sans-serif", vec![named!("Times New Roman"), SANS_SERIF]);
test!(font_family_10, "Arial, sans-serif, 'fantasy'", vec![named!("Arial"), SANS_SERIF, named!("fantasy")]);
test!(font_family_11, " Arial , monospace , 'fantasy'", vec![named!("Arial"), MONOSPACE, named!("fantasy")]);
test!(font_family_12, "Times New Roman", vec![named!("Times New Roman")]);
test!(font_family_13, "\"Times New Roman\", sans-serif, sans-serif, \"Arial\"",
vec![named!("Times New Roman"), SANS_SERIF, SANS_SERIF, named!("Arial")]
);
test!(font_family_14, "Times New Roman,,,Arial", vec![named!("Times New Roman"), named!("Arial")]);
test!(font_family_15, "简体中文,sans-serif , ,\"日本語フォント\",Arial",
vec![named!("简体中文"), SANS_SERIF, named!("日本語フォント"), named!("Arial")]);

test!(font_family_16, "", vec![]);

macro_rules! font_family_err {
($name:ident, $text:expr, $result:expr) => (
#[test]
fn $name() {
assert_eq!(parse_font_families($text).unwrap_err().to_string(), $result);
}
)
}
font_family_err!(font_family_err_1, "Red/Black, sans-serif", "invalid ident");
font_family_err!(font_family_err_2, "\"Lucida\" Grande, sans-serif", "unexpected data at position 10");
font_family_err!(font_family_err_3, "Ahem!, sans-serif", "invalid ident");
font_family_err!(font_family_err_4, "test@foo, sans-serif", "invalid ident");
font_family_err!(font_family_err_5, "#POUND, sans-serif", "invalid ident");
font_family_err!(font_family_err_6, "Hawaii 5-0, sans-serif", "invalid ident");
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ mod directional_position;
mod enable_background;
mod error;
mod filter_functions;
mod font;
mod funciri;
mod length;
mod number;
Expand All @@ -91,6 +92,7 @@ pub use crate::directional_position::*;
pub use crate::enable_background::*;
pub use crate::error::*;
pub use crate::filter_functions::*;
pub use crate::font::*;
pub use crate::funciri::*;
pub use crate::length::*;
pub use crate::number::*;
Expand Down
2 changes: 1 addition & 1 deletion src/paint_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl std::str::FromStr for PaintOrder {
let mut s = Stream::from(text);
while !s.at_end() && order.len() < 3 {
s.skip_spaces();
let name = s.consume_ident();
let name = s.consume_ascii_ident();
s.skip_spaces();
let name = match name {
// `normal` is the special value that should short-circuit.
Expand Down
Loading
Loading