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