1use super::layout::{Display, Float, ListStyleType, TextAlign};
2use super::layout::{FontKind, FontStyle, FontWeight, WordSpacing};
3use super::layout::{GlueMaterial, InlineMaterial, PenaltyMaterial};
4use crate::color::{Color, BLACK, WHITE};
5use crate::geom::Edge;
6use crate::unit::{in_to_px, mm_to_px, pc_to_px, pt_to_px};
7use crate::unit::{CENTIMETERS_PER_INCH, MILLIMETERS_PER_INCH, PICAS_PER_INCH, POINTS_PER_INCH};
8use fxhash::FxHashSet;
9use regex::Regex;
10
11const SIZE_FACTOR: f32 = 1.26;
12const ABSOLUTE_SIZE_KEYWORDS: [&str; 7] = [
13 "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
14];
15const RELATIVE_SIZE_KEYWORDS: [&str; 2] = ["smaller", "larger"];
16
17pub fn parse_length(value: &str, em: f32, rem: f32, dpi: u16) -> Option<i32> {
19 if let Some(index) = value.find(|c: char| c.is_ascii_alphabetic()) {
20 value[..index]
21 .parse()
22 .ok()
23 .and_then(|v| match &value[index..] {
24 "em" => Some(pt_to_px(v * em, dpi).round() as i32),
25 "rem" => Some(pt_to_px(v * rem, dpi).round() as i32),
26 "pt" => Some(pt_to_px(v, dpi).round() as i32),
27 "pc" => Some(pc_to_px(v, dpi).round() as i32),
28 "cm" => Some(mm_to_px(10.0 * v, dpi).round() as i32),
29 "mm" => Some(mm_to_px(v, dpi).round() as i32),
30 "in" => Some(in_to_px(v, dpi).round() as i32),
31 "px" => Some(pt_to_px(v * 0.75, dpi).round() as i32),
32 _ => None,
33 })
34 } else if value == "0" {
35 Some(0)
36 } else {
37 None
38 }
39}
40
41pub fn parse_font_size(value: &str, em: f32, rem: f32) -> Option<f32> {
43 if value.find(|c: char| c.is_ascii_digit()).is_some() {
44 if let Some(index) = value.find(|c: char| c.is_ascii_alphabetic()) {
45 value[..index].parse().ok().and_then(|v| {
46 if v <= 0.0 {
47 return None;
48 }
49 match &value[index..] {
50 "em" => Some(v * em),
51 "rem" => Some(v * rem),
52 "pt" => Some(v),
53 "pc" => Some(v * POINTS_PER_INCH / PICAS_PER_INCH),
54 "cm" => Some(v * POINTS_PER_INCH / CENTIMETERS_PER_INCH),
55 "mm" => Some(v * POINTS_PER_INCH / MILLIMETERS_PER_INCH),
56 "in" => Some(v * POINTS_PER_INCH),
57 "px" => Some(v * 0.75),
58 _ => None,
59 }
60 })
61 } else if let Some(percent) = value.strip_suffix('%') {
62 percent.parse::<f32>().ok().map(|v| v / 100.0 * em)
63 } else {
64 None
65 }
66 } else if let Some(index) = ABSOLUTE_SIZE_KEYWORDS.iter().position(|&v| v == value) {
67 let e = index as i32 - 3;
68 Some(SIZE_FACTOR.powi(e) * rem)
69 } else if let Some(index) = RELATIVE_SIZE_KEYWORDS.iter().position(|&v| v == value) {
70 let e = (2 * index) as i32 - 1;
71 Some(SIZE_FACTOR.powi(e) * em)
72 } else {
73 None
74 }
75}
76
77pub fn parse_inline_material(value: &str, em: f32, rem: f32, dpi: u16) -> Vec<InlineMaterial> {
78 let mut inlines = Vec::new();
79 for decl in value.split(',') {
80 let tokens: Vec<&str> = decl.trim().split_whitespace().collect();
81 match tokens.get(0).cloned() {
82 Some("glue") => {
83 let width = tokens
84 .get(1)
85 .and_then(|s| parse_length(s, em, rem, dpi))
86 .unwrap_or(0);
87 let stretch = tokens
88 .get(2)
89 .and_then(|s| parse_length(s, em, rem, dpi))
90 .unwrap_or(0);
91 let shrink = tokens
92 .get(3)
93 .and_then(|s| parse_length(s, em, rem, dpi))
94 .unwrap_or(0);
95 inlines.push(InlineMaterial::Glue(GlueMaterial {
96 width,
97 stretch,
98 shrink,
99 }));
100 }
101 Some("penalty") => {
102 let penalty = tokens.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
103 let flagged = tokens.get(2).and_then(|s| s.parse().ok()).unwrap_or(false);
104 let width = tokens
105 .get(3)
106 .and_then(|s| parse_length(s, em, rem, dpi))
107 .unwrap_or(0);
108 inlines.push(InlineMaterial::Penalty(PenaltyMaterial {
109 width,
110 penalty,
111 flagged,
112 }));
113 }
114 Some("box") => {
115 let width = tokens
116 .get(1)
117 .and_then(|s| parse_length(s, em, rem, dpi))
118 .unwrap_or(0);
119 inlines.push(InlineMaterial::Box(width));
120 }
121 _ => (),
122 }
123 }
124 inlines
125}
126
127pub fn parse_font_kind(value: &str) -> Option<FontKind> {
128 value
129 .split(',')
130 .last()
131 .map(str::trim)
132 .and_then(|v| match v {
133 "serif" => Some(FontKind::Serif),
134 "sans-serif" => Some(FontKind::SansSerif),
135 "monospace" => Some(FontKind::Monospace),
136 "cursive" => Some(FontKind::Cursive),
137 "fantasy" => Some(FontKind::Fantasy),
138 _ => None,
139 })
140}
141
142pub fn parse_letter_spacing(value: &str, em: f32, rem: f32, dpi: u16) -> Option<i32> {
143 if value == "normal" {
144 Some(0)
145 } else {
146 parse_length(value, em, rem, dpi)
147 }
148}
149
150pub fn parse_word_spacing(value: &str, em: f32, rem: f32, dpi: u16) -> Option<WordSpacing> {
151 if value == "normal" {
152 Some(WordSpacing::Normal)
153 } else if let Some(percent) = value.strip_suffix('%') {
154 percent
155 .parse::<f32>()
156 .ok()
157 .map(|v| WordSpacing::Ratio(v / 100.0))
158 } else {
159 parse_length(value, em, rem, dpi).map(WordSpacing::Length)
160 }
161}
162
163pub fn parse_vertical_align(
164 value: &str,
165 em: f32,
166 rem: f32,
167 line_height: i32,
168 dpi: u16,
169) -> Option<i32> {
170 if value == "baseline" {
171 Some(0)
172 } else if value == "super" || value == "top" {
173 Some(pt_to_px(0.4 * em, dpi).round() as i32)
174 } else if value == "sub" || value == "bottom" {
175 Some(pt_to_px(-0.2 * em, dpi).round() as i32)
176 } else if let Some(percent) = value.strip_suffix('%') {
177 percent
178 .parse::<f32>()
179 .ok()
180 .map(|v| (v / 100.0 * line_height as f32) as i32)
181 } else {
182 parse_length(value, em, rem, dpi)
183 }
184}
185
186pub fn parse_font_weight(value: &str) -> Option<FontWeight> {
187 if value == "normal" {
188 Some(FontWeight::Normal)
189 } else if value == "bold" {
190 Some(FontWeight::Bold)
191 } else {
192 None
193 }
194}
195
196pub fn parse_font_style(value: &str) -> Option<FontStyle> {
197 if value == "normal" {
198 Some(FontStyle::Normal)
199 } else if value == "italic" {
200 Some(FontStyle::Italic)
201 } else {
202 None
203 }
204}
205
206pub fn parse_display(value: &str) -> Option<Display> {
207 match value {
208 "block" => Some(Display::Block),
209 "inline" => Some(Display::Inline),
210 "inline-table" => Some(Display::InlineTable),
211 "none" => Some(Display::None),
212 _ => None,
213 }
214}
215
216pub fn parse_float(value: &str) -> Option<Float> {
217 match value {
218 "left" => Some(Float::Left),
219 "right" => Some(Float::Right),
220 _ => None,
221 }
222}
223
224pub fn parse_list_style_type(value: &str) -> Option<ListStyleType> {
225 match value {
226 "none" => Some(ListStyleType::None),
227 "disc" => Some(ListStyleType::Disc),
228 "circle" => Some(ListStyleType::Circle),
229 "square" => Some(ListStyleType::Square),
230 "decimal" => Some(ListStyleType::Decimal),
231 "lower-roman" => Some(ListStyleType::LowerRoman),
232 "upper-roman" => Some(ListStyleType::UpperRoman),
233 "lower-alpha" | "lower-latin" => Some(ListStyleType::LowerAlpha),
234 "upper-alpha" | "upper-latin" => Some(ListStyleType::UpperAlpha),
235 "lower-greek" => Some(ListStyleType::LowerGreek),
236 "upper-greek" => Some(ListStyleType::UpperGreek),
237 _ => None,
238 }
239}
240
241pub fn parse_width(value: &str, em: f32, rem: f32, width: i32, dpi: u16) -> Option<i32> {
242 if value == "auto" {
243 Some(0)
244 } else if let Some(percent) = value.strip_suffix('%') {
245 percent
246 .parse::<f32>()
247 .ok()
248 .map(|v| (v / 100.0 * width as f32) as i32)
249 } else {
250 parse_length(value, em, rem, dpi)
251 }
252}
253
254pub fn parse_height(value: &str, em: f32, rem: f32, width: i32, dpi: u16) -> Option<i32> {
255 if value == "auto" {
256 Some(0)
257 } else if let Some(percent) = value.strip_suffix('%') {
258 percent
259 .parse::<f32>()
260 .ok()
261 .map(|v| (v / 100.0 * width as f32) as i32)
262 } else {
263 parse_length(value, em, rem, dpi)
264 }
265}
266
267fn parse_edge_length(value: &str, em: f32, rem: f32, width: i32, auto_value: i32, dpi: u16) -> i32 {
268 if value == "auto" {
269 auto_value
270 } else if value == "0" {
271 0
272 } else if let Some(percent) = value.strip_suffix('%') {
273 percent
274 .parse::<f32>()
275 .ok()
276 .map(|v| (v / 100.0 * width as f32) as i32)
277 .unwrap_or_default()
278 } else {
279 parse_length(value, em, rem, dpi).unwrap_or_default()
280 }
281}
282
283pub fn parse_edge(
284 top_edge: Option<&str>,
285 right_edge: Option<&str>,
286 bottom_edge: Option<&str>,
287 left_edge: Option<&str>,
288 em: f32,
289 rem: f32,
290 width: i32,
291 dpi: u16,
292) -> Edge {
293 let mut e = Edge::default();
294
295 if let Some(v) = top_edge {
296 e.top = parse_edge_length(v, em, rem, width, 0, dpi);
297 }
298
299 if let Some(v) = right_edge {
300 e.right = parse_edge_length(v, em, rem, width, width, dpi);
301 }
302
303 if let Some(v) = bottom_edge {
304 e.bottom = parse_edge_length(v, em, rem, width, 0, dpi);
305 }
306
307 if let Some(v) = left_edge {
308 e.left = parse_edge_length(v, em, rem, width, width, dpi);
309 }
310
311 e
312}
313
314pub fn parse_text_align(value: &str) -> Option<TextAlign> {
315 match value {
316 "justify" => Some(TextAlign::Justify),
317 "left" => Some(TextAlign::Left),
318 "right" => Some(TextAlign::Right),
319 "center" => Some(TextAlign::Center),
320 _ => None,
321 }
322}
323
324pub fn parse_line_height(value: &str, em: f32, rem: f32, dpi: u16) -> Option<i32> {
325 if value == "normal" {
326 Some(pt_to_px(1.2 * em, dpi).round() as i32)
327 } else if let Some(percent) = value.strip_suffix('%') {
328 percent
329 .parse::<f32>()
330 .ok()
331 .map(|v| pt_to_px(v / 100.0 * em as f32, dpi).round() as i32)
332 } else if value.ends_with(|c: char| !c.is_ascii_alphabetic()) {
333 value
334 .parse::<f32>()
335 .ok()
336 .map(|v| pt_to_px(v * em, dpi).round() as i32)
337 } else {
338 parse_length(value, em, rem, dpi)
339 }
340}
341
342pub fn parse_text_indent(value: &str, em: f32, rem: f32, width: i32, dpi: u16) -> Option<i32> {
343 if let Some(percent) = value.strip_suffix('%') {
344 percent
345 .parse::<f32>()
346 .ok()
347 .map(|v| (v / 100.0 * width as f32) as i32)
348 } else {
349 parse_length(value, em, rem, dpi)
350 }
351}
352
353pub fn parse_font_features(value: &str) -> Vec<String> {
354 let re = Regex::new(r#""([^"]+)"\s*(on|off|\d+)?"#).unwrap();
355 let mut features = Vec::new();
356
357 for cap in re.captures_iter(value) {
358 let mut name = cap[1].to_string();
359 let value = cap.get(2).map_or("", |m| m.as_str());
360 match value {
361 "off" | "0" => name = format!("-{}", name),
362 "on" | "1" | "" => (),
363 _ => name = format!("{}={}", name, value),
364 }
365 features.push(name);
366 }
367
368 features
369}
370
371pub fn parse_font_variant(value: &str) -> Vec<String> {
372 let mut features = FxHashSet::default();
373
374 for name in value.split_whitespace() {
375 match name {
376 "small-caps" => {
377 features.insert("smcp");
378 }
379 "all-small-caps" => {
380 features.insert("smcp");
381 features.insert("c2sc");
382 }
383 "oldstyle-nums" => {
384 features.insert("onum");
385 }
386 "lining-nums" => {
387 features.insert("lnum");
388 }
389 "tabular-nums" => {
390 features.insert("tnum");
391 }
392 "proportional-nums" => {
393 features.insert("pnum");
394 }
395 "contextual" => {
396 features.insert("clig");
397 }
398 "discretionary-ligatures" => {
399 features.insert("clig");
400 features.insert("dlig");
401 }
402 "slashed-zero" => {
403 features.insert("zero");
404 }
405 _ => (),
406 }
407 }
408
409 features.into_iter().map(String::from).collect()
410}
411
412pub fn parse_color(value: &str) -> Option<Color> {
413 if value.starts_with('#') {
414 if value.len() < 4 {
415 return None;
416 }
417 let chunk_size = if value.len() < 7 { 1 } else { 2 };
418 let red = u8::from_str_radix(&value[1..=chunk_size].repeat(3 - chunk_size), 16).ok()?;
419 let green = u8::from_str_radix(
420 &value[chunk_size + 1..=2 * chunk_size].repeat(3 - chunk_size),
421 16,
422 )
423 .ok()?;
424 let blue = u8::from_str_radix(
425 &value[2 * chunk_size + 1..=3 * chunk_size].repeat(3 - chunk_size),
426 16,
427 )
428 .ok()?;
429 let color = Color::from_rgb(&[red, green, blue]);
430 Some(color)
431 } else {
432 match value {
433 "black" => Some(BLACK),
434 "white" => Some(WHITE),
435 "gray" | "grey" => parse_color("#888"),
436 "silver" => parse_color("#c0c0c0"),
437 "red" => parse_color("#f00"),
438 "maroon" => parse_color("#800000"),
439 "orange" => parse_color("#ffA500"),
440 "yellow" => parse_color("#ff0"),
441 "olive" => parse_color("#808000"),
442 "lime" => parse_color("#0f0"),
443 "green" => parse_color("#008000"),
444 "aqua" | "cyan" => parse_color("#0ff"),
445 "teal" => parse_color("#008080"),
446 "blue" => parse_color("#00f"),
447 "navy" => parse_color("#000080"),
448 "fuchsia" | "magenta" => parse_color("#f0f"),
449 "purple" => parse_color("#800080"),
450 _ => None,
451 }
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_parse_color() {
461 let a = parse_color("#000");
462 let b = parse_color("#f00");
463 let c = parse_color("#0f0");
464 let d = parse_color("#00f");
465 let e = parse_color("#fff");
466 assert_eq!(a, Some(Color::Rgb(0, 0, 0)));
467 assert_eq!(b, Some(Color::Rgb(255, 0, 0)));
468 assert_eq!(c, Some(Color::Rgb(0, 255, 0)));
469 assert_eq!(d, Some(Color::Rgb(0, 0, 255)));
470 assert_eq!(e, Some(Color::Rgb(255, 255, 255)));
471 }
472}