cadmus_core/document/html/
parse.rs

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
17// TODO: vh, vw, vmin, vmax?
18pub 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
41// Input and output sizes are in points.
42pub 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}