1use crate::stream::{ByteExt, Stream};
2use crate::Error;
3use std::fmt::Display;
4
5/// Parses a list of font families and generic families from a string.
6pub fn parse_font_families(text: &str) -> Result<Vec<FontFamily>, Error> {
7 let mut s: Stream<'_> = Stream::from(text);
8 let font_families: Vec = s.parse_font_families()?;
9
10 s.skip_spaces();
11 if !s.at_end() {
12 return Err(Error::UnexpectedData(s.calc_char_pos()));
13 }
14
15 Ok(font_families)
16}
17
18/// A type of font family.
19#[derive(Clone, PartialEq, Eq, Debug, Hash)]
20pub enum FontFamily {
21 /// A serif font.
22 Serif,
23 /// A sans-serif font.
24 SansSerif,
25 /// A cursive font.
26 Cursive,
27 /// A fantasy font.
28 Fantasy,
29 /// A monospace font.
30 Monospace,
31 /// A custom named font.
32 Named(String),
33}
34
35impl Display for FontFamily {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 let str: String = match self {
38 FontFamily::Monospace => "monospace".to_string(),
39 FontFamily::Serif => "serif".to_string(),
40 FontFamily::SansSerif => "sans-serif".to_string(),
41 FontFamily::Cursive => "cursive".to_string(),
42 FontFamily::Fantasy => "fantasy".to_string(),
43 FontFamily::Named(s: &String) => format!("\"{}\"", s),
44 };
45 write!(f, "{}", str)
46 }
47}
48
49impl<'a> Stream<'a> {
50 pub fn parse_font_families(&mut self) -> Result<Vec<FontFamily>, Error> {
51 let mut families = vec![];
52
53 while !self.at_end() {
54 self.skip_spaces();
55
56 let family = {
57 let ch = self.curr_byte()?;
58 if ch == b'\'' || ch == b'\"' {
59 let res = self.parse_quoted_string()?;
60 FontFamily::Named(res.to_string())
61 } else {
62 let mut idents = vec![];
63
64 while let Some(c) = self.chars().next() {
65 if c != ',' {
66 idents.push(self.parse_ident()?.to_string());
67 self.skip_spaces();
68 } else {
69 break;
70 }
71 }
72
73 let joined = idents.join(" ");
74
75 // TODO: No CSS keyword must be matched as a family name...
76 match joined.as_str() {
77 "serif" => FontFamily::Serif,
78 "sans-serif" => FontFamily::SansSerif,
79 "cursive" => FontFamily::Cursive,
80 "fantasy" => FontFamily::Fantasy,
81 "monospace" => FontFamily::Monospace,
82 _ => FontFamily::Named(joined),
83 }
84 }
85 };
86
87 families.push(family);
88
89 if let Ok(b) = self.curr_byte() {
90 if b == b',' {
91 self.advance(1);
92 } else {
93 break;
94 }
95 }
96 }
97
98 let families = families
99 .into_iter()
100 .filter(|f| match f {
101 FontFamily::Named(s) => !s.is_empty(),
102 _ => true,
103 })
104 .collect();
105
106 Ok(families)
107 }
108}
109
110/// The values of a [`font` shorthand](https://www.w3.org/TR/css-fonts-3/#font-prop).
111#[derive(Clone, PartialEq, Eq, Debug, Hash)]
112pub struct FontShorthand<'a> {
113 /// The font style.
114 pub font_style: Option<&'a str>,
115 /// The font variant.
116 pub font_variant: Option<&'a str>,
117 /// The font weight.
118 pub font_weight: Option<&'a str>,
119 /// The font stretch.
120 pub font_stretch: Option<&'a str>,
121 /// The font size.
122 pub font_size: &'a str,
123 /// The font family.
124 pub font_family: &'a str,
125}
126
127impl<'a> FontShorthand<'a> {
128 /// Parses the `font` shorthand from a string.
129 ///
130 /// We can't use the `FromStr` trait because it requires
131 /// an owned value as a return type.
132 ///
133 /// [font]: https://www.w3.org/TR/css-fonts-3/#font-prop
134 pub fn from_str(text: &'a str) -> Result<Self, Error> {
135 let mut stream = Stream::from(text);
136 stream.skip_spaces();
137
138 let mut prev_pos = stream.pos();
139
140 let mut font_style = None;
141 let mut font_variant = None;
142 let mut font_weight = None;
143 let mut font_stretch = None;
144
145 for _ in 0..4 {
146 let ident = stream.consume_ascii_ident();
147
148 match ident {
149 // TODO: Reuse actual parsers to prevent duplication.
150 // We ignore normal because it's ambiguous to which it belongs and all
151 // other attributes need to be resetted anyway.
152 "normal" => {}
153 "small-caps" => font_variant = Some(ident),
154 "italic" | "oblique" => font_style = Some(ident),
155 "bold" | "bolder" | "lighter" | "100" | "200" | "300" | "400" | "500" | "600"
156 | "700" | "800" | "900" => font_weight = Some(ident),
157 "ultra-condensed" | "extra-condensed" | "condensed" | "semi-condensed"
158 | "semi-expanded" | "expanded" | "extra-expanded" | "ultra-expanded" => {
159 font_stretch = Some(ident)
160 }
161 _ => {
162 // Not one of the 4 properties, so we backtrack and then start pasing font
163 // size and family.
164 stream = Stream::from(text);
165 stream.advance(prev_pos);
166 break;
167 }
168 }
169
170 stream.skip_spaces();
171 prev_pos = stream.pos();
172 }
173
174 prev_pos = stream.pos();
175 if stream.curr_byte()?.is_digit() {
176 // A font size such as '15pt'.
177 let _ = stream.parse_length()?;
178 } else {
179 // A font size like 'xx-large'.
180 let size = stream.consume_ascii_ident();
181
182 if !matches!(
183 size,
184 "xx-small"
185 | "x-small"
186 | "small"
187 | "medium"
188 | "large"
189 | "x-large"
190 | "xx-large"
191 | "larger"
192 | "smaller"
193 ) {
194 return Err(Error::UnexpectedData(prev_pos));
195 }
196 }
197
198 let font_size = stream.slice_back(prev_pos);
199 stream.skip_spaces();
200
201 if stream.curr_byte()? == b'/' {
202 // We should ignore line height since it has no effect in SVG.
203 stream.advance(1);
204 stream.skip_spaces();
205 let _ = stream.parse_length()?;
206 stream.skip_spaces();
207 }
208
209 if stream.at_end() {
210 return Err(Error::UnexpectedEndOfStream);
211 }
212
213 let font_family = stream.slice_tail();
214
215 Ok(Self {
216 font_style,
217 font_variant,
218 font_weight,
219 font_stretch,
220 font_size,
221 font_family,
222 })
223 }
224}
225
226#[rustfmt::skip]
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 macro_rules! font_family {
232 ($name:ident, $text:expr, $result:expr) => (
233 #[test]
234 fn $name() {
235 assert_eq!(parse_font_families($text).unwrap(), $result);
236 }
237 )
238 }
239
240 macro_rules! named {
241 ($text:expr) => (
242 FontFamily::Named($text.to_string())
243 )
244 }
245
246 const SERIF: FontFamily = FontFamily::Serif;
247 const SANS_SERIF: FontFamily = FontFamily::SansSerif;
248 const FANTASY: FontFamily = FontFamily::Fantasy;
249 const MONOSPACE: FontFamily = FontFamily::Monospace;
250 const CURSIVE: FontFamily = FontFamily::Cursive;
251
252 font_family!(font_family_1, "Times New Roman", vec![named!("Times New Roman")]);
253 font_family!(font_family_2, "serif", vec![SERIF]);
254 font_family!(font_family_3, "sans-serif", vec![SANS_SERIF]);
255 font_family!(font_family_4, "cursive", vec![CURSIVE]);
256 font_family!(font_family_5, "fantasy", vec![FANTASY]);
257 font_family!(font_family_6, "monospace", vec![MONOSPACE]);
258 font_family!(font_family_7, "'Times New Roman'", vec![named!("Times New Roman")]);
259 font_family!(font_family_8, "'Times New Roman', sans-serif", vec![named!("Times New Roman"), SANS_SERIF]);
260 font_family!(font_family_9, "'Times New Roman', sans-serif", vec![named!("Times New Roman"), SANS_SERIF]);
261 font_family!(font_family_10, "Arial, sans-serif, 'fantasy'", vec![named!("Arial"), SANS_SERIF, named!("fantasy")]);
262 font_family!(font_family_11, " Arial , monospace , 'fantasy'", vec![named!("Arial"), MONOSPACE, named!("fantasy")]);
263 font_family!(font_family_12, "Times New Roman", vec![named!("Times New Roman")]);
264 font_family!(font_family_13, "\"Times New Roman\", sans-serif, sans-serif, \"Arial\"",
265 vec![named!("Times New Roman"), SANS_SERIF, SANS_SERIF, named!("Arial")]
266 );
267 font_family!(font_family_14, "Times New Roman,,,Arial", vec![named!("Times New Roman"), named!("Arial")]);
268 font_family!(font_family_15, "简体中文,sans-serif , ,\"日本語フォント\",Arial",
269 vec![named!("简体中文"), SANS_SERIF, named!("日本語フォント"), named!("Arial")]);
270
271 font_family!(font_family_16, "", vec![]);
272
273 macro_rules! font_family_err {
274 ($name:ident, $text:expr, $result:expr) => (
275 #[test]
276 fn $name() {
277 assert_eq!(parse_font_families($text).unwrap_err().to_string(), $result);
278 }
279 )
280 }
281 font_family_err!(font_family_err_1, "Red/Black, sans-serif", "invalid ident");
282 font_family_err!(font_family_err_2, "\"Lucida\" Grande, sans-serif", "unexpected data at position 10");
283 font_family_err!(font_family_err_3, "Ahem!, sans-serif", "invalid ident");
284 font_family_err!(font_family_err_4, "test@foo, sans-serif", "invalid ident");
285 font_family_err!(font_family_err_5, "#POUND, sans-serif", "invalid ident");
286 font_family_err!(font_family_err_6, "Hawaii 5-0, sans-serif", "invalid ident");
287
288 impl<'a> FontShorthand<'a> {
289 fn new(font_style: Option<&'a str>, font_variant: Option<&'a str>, font_weight: Option<&'a str>,
290 font_stretch: Option<&'a str>, font_size: &'a str, font_family: &'a str) -> Self {
291 Self {
292 font_style, font_variant, font_weight, font_stretch, font_size, font_family
293 }
294 }
295 }
296
297 macro_rules! font_shorthand {
298 ($name:ident, $text:expr, $result:expr) => (
299 #[test]
300 fn $name() {
301 assert_eq!(FontShorthand::from_str($text).unwrap(), $result);
302 }
303 )
304 }
305
306 font_shorthand!(font_shorthand_1, "12pt/14pt sans-serif",
307 FontShorthand::new(None, None, None, None, "12pt", "sans-serif"));
308 font_shorthand!(font_shorthand_2, "80% sans-serif",
309 FontShorthand::new(None, None, None, None, "80%", "sans-serif"));
310 font_shorthand!(font_shorthand_3, "bold italic large Palatino, serif",
311 FontShorthand::new(Some("italic"), None, Some("bold"), None, "large", "Palatino, serif"));
312 font_shorthand!(font_shorthand_4, "x-large/110% \"new century schoolbook\", serif",
313 FontShorthand::new(None, None, None, None, "x-large", "\"new century schoolbook\", serif"));
314 font_shorthand!(font_shorthand_5, "normal small-caps 120%/120% fantasy",
315 FontShorthand::new(None, Some("small-caps"), None, None, "120%", "fantasy"));
316 font_shorthand!(font_shorthand_6, "condensed oblique 12pt \"Helvetica Neue\", serif",
317 FontShorthand::new(Some("oblique"), None, None, Some("condensed"), "12pt", "\"Helvetica Neue\", serif"));
318 font_shorthand!(font_shorthand_7, "italic 500 2em sans-serif, 'Noto Sans'",
319 FontShorthand::new(Some("italic"), None, Some("500"), None, "2em", "sans-serif, 'Noto Sans'"));
320 font_shorthand!(font_shorthand_8, "xx-large 'Noto Sans'",
321 FontShorthand::new(None, None, None, None, "xx-large", "'Noto Sans'"));
322 font_shorthand!(font_shorthand_9, "small-caps normal normal italic xx-small Times",
323 FontShorthand::new(Some("italic"), Some("small-caps"), None, None, "xx-small", "Times"));
324
325
326 macro_rules! font_shorthand_err {
327 ($name:ident, $text:expr, $result:expr) => (
328 #[test]
329 fn $name() {
330 assert_eq!(FontShorthand::from_str($text).unwrap_err(), $result);
331 }
332 )
333 }
334
335 font_shorthand_err!(font_shorthand_err_1, "", Error::UnexpectedEndOfStream);
336 font_shorthand_err!(font_shorthand_err_2, "Noto Sans", Error::UnexpectedData(0));
337 font_shorthand_err!(font_shorthand_err_3, "12pt ", Error::UnexpectedEndOfStream);
338 font_shorthand_err!(font_shorthand_err_4, "something 12pt 'Noto Sans'", Error::UnexpectedData(0));
339 font_shorthand_err!(font_shorthand_err_5, "'Noto Sans' 13pt", Error::UnexpectedData(0));
340 font_shorthand_err!(font_shorthand_err_6,
341 "small-caps normal normal normal italic xx-large Times", Error::UnexpectedData(32));
342}
343