1 | use crate::stream::{ByteExt, Stream}; |
2 | use crate::Error; |
3 | use std::fmt::Display; |
4 | |
5 | /// Parses a list of font families and generic families from a string. |
6 | pub 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)] |
20 | pub 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 | |
35 | impl 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 | |
49 | impl<'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)] |
112 | pub 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 | |
127 | impl<'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)] |
228 | mod 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 | |