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