1
//! CSS color values.
2

            
3
use cssparser::{hsl_to_rgb, hwb_to_rgb, Color, ParseErrorKind, Parser, RGBA};
4

            
5
use crate::error::*;
6
use crate::parsers::Parse;
7

            
8
/// Turn a short-lived [`cssparser::ParseError`] into a long-lived [`ParseError`].
9
///
10
/// cssparser's error type has a lifetime equal to the string being parsed.  We want
11
/// a long-lived error so we can store it away if needed.  Basically, here we turn
12
/// a `&str` into a `String`.
13
1076
fn map_color_parse_error(err: cssparser::ParseError<'_, ()>) -> ParseError<'_> {
14
1076
    let string_err = match err.kind {
15
1076
        ParseErrorKind::Basic(ref e) => format!("{}", e),
16
        ParseErrorKind::Custom(()) => {
17
            // In cssparser 0.31, the error type for Color::parse is defined like this:
18
            //
19
            //   pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> {
20
            //
21
            // The ParseError<'i, ()> means that the ParseErrorKind::Custom(T) variant will have
22
            // T be the () type.
23
            //
24
            // So, here we match for () inside the Custom variant.  If cssparser
25
            // changes its error API, this match will hopefully catch errors.
26
            //
27
            // Implementation detail: Color::parse() does not ever return Custom errors, only
28
            // Basic ones.  So the match for Basic above handles everything, and this one
29
            // for () is a dummy case.
30
            "could not parse color".to_string()
31
        }
32
    };
33

            
34
1076
    ParseError {
35
1076
        kind: ParseErrorKind::Custom(ValueErrorKind::Parse(string_err)),
36
1076
        location: err.location,
37
    }
38
1076
}
39

            
40
1033838
fn parse_plain_color<'i>(parser: &mut Parser<'i, '_>) -> Result<cssparser::Color, ParseError<'i>> {
41
1033838
    let loc = parser.current_source_location();
42

            
43
1033838
    let color = cssparser::Color::parse(parser).map_err(map_color_parse_error)?;
44

            
45
    // Return only supported color types, and mark the others as errors.
46
1032762
    match color {
47
1032752
        Color::CurrentColor | Color::Rgba(_) | Color::Hsl(_) | Color::Hwb(_) => Ok(color),
48

            
49
10
        _ => Err(ParseError {
50
10
            kind: ParseErrorKind::Custom(ValueErrorKind::parse_error("unsupported color syntax")),
51
            location: loc,
52
10
        }),
53
    }
54
1033838
}
55

            
56
/// Parse a custom property name.
57
///
58
/// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name>
59
7
fn parse_name(s: &str) -> Result<&str, ()> {
60
7
    if s.starts_with("--") && s.len() > 2 {
61
7
        Ok(&s[2..])
62
    } else {
63
        Err(())
64
    }
65
7
}
66

            
67
7
fn parse_var_with_fallback<'i>(
68
    parser: &mut Parser<'i, '_>,
69
) -> Result<cssparser::Color, ParseError<'i>> {
70
7
    let name = parser.expect_ident_cloned()?;
71

            
72
    // ignore the name for now; we'll use it later when we actually
73
    // process the names of custom variables
74
7
    let _name = parse_name(&name).map_err(|()| {
75
        parser.new_custom_error(ValueErrorKind::parse_error(&format!(
76
            "unexpected identifier {}",
77
            name
78
        )))
79
    })?;
80

            
81
14
    parser.expect_comma()?;
82

            
83
    // FIXME: when fixing #459 (full support for var()), note that
84
    // https://drafts.csswg.org/css-variables/#using-variables indicates that var(--a,) is
85
    // a valid function, which means that the fallback value is an empty set of tokens.
86
    //
87
    // Also, see Servo's extra code to handle semicolons and stuff in toplevel rules.
88
    //
89
    // Also, tweak the tests tagged with "FIXME: var()" below.
90

            
91
6
    parse_plain_color(parser)
92
7
}
93

            
94
impl Parse for cssparser::Color {
95
1033841
    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<cssparser::Color, ParseError<'i>> {
96
2067686
        if let Ok(c) = parser.try_parse(|p| {
97
1033845
            p.expect_function_matching("var")?;
98
7
            p.parse_nested_block(parse_var_with_fallback)
99
1033845
        }) {
100
2
            Ok(c)
101
        } else {
102
1033839
            parse_plain_color(parser)
103
        }
104
1033841
    }
105
}
106

            
107
/// Normalizes `h` (a hue value in degrees) to be in the interval `[0.0, 1.0]`.
108
///
109
/// Rust-cssparser (the cssparser-color crate) provides
110
/// [`hsl_to_rgb()`], but it assumes that the hue is between 0 and 1.
111
/// `normalize_hue()` takes a value with respect to a scale of 0 to
112
/// 360 degrees and converts it to that different scale.
113
19
fn normalize_hue(h: f32) -> f32 {
114
19
    h.rem_euclid(360.0) / 360.0
115
19
}
116

            
117
949897
pub fn color_to_rgba(color: &Color) -> RGBA {
118
949897
    match color {
119
949884
        Color::Rgba(rgba) => *rgba,
120

            
121
7
        Color::Hsl(hsl) => {
122
7
            let hue = normalize_hue(hsl.hue.unwrap_or(0.0));
123
7
            let (red, green, blue) = hsl_to_rgb(
124
                hue,
125
7
                hsl.saturation.unwrap_or(0.0),
126
7
                hsl.lightness.unwrap_or(0.0),
127
            );
128

            
129
7
            RGBA::from_floats(Some(red), Some(green), Some(blue), hsl.alpha)
130
        }
131

            
132
6
        Color::Hwb(hwb) => {
133
6
            let hue = normalize_hue(hwb.hue.unwrap_or(0.0));
134
6
            let (red, green, blue) = hwb_to_rgb(
135
                hue,
136
6
                hwb.whiteness.unwrap_or(0.0),
137
6
                hwb.blackness.unwrap_or(0.0),
138
            );
139

            
140
6
            RGBA::from_floats(Some(red), Some(green), Some(blue), hwb.alpha)
141
        }
142

            
143
        _ => unimplemented!(),
144
    }
145
949897
}
146

            
147
#[cfg(test)]
148
mod tests {
149
    use super::*;
150

            
151
    #[test]
152
2
    fn parses_plain_color() {
153
1
        assert_eq!(
154
1
            Color::parse_str("#112233").unwrap(),
155
1
            Color::Rgba(RGBA::new(Some(0x11), Some(0x22), Some(0x33), Some(1.0)))
156
        );
157
2
    }
158

            
159
    #[test]
160
2
    fn var_with_fallback_parses_as_color() {
161
1
        assert_eq!(
162
1
            Color::parse_str("var(--foo, #112233)").unwrap(),
163
1
            Color::Rgba(RGBA::new(Some(0x11), Some(0x22), Some(0x33), Some(1.0)))
164
        );
165

            
166
1
        assert_eq!(
167
1
            Color::parse_str("var(--foo, rgb(100% 50% 25%)").unwrap(),
168
1
            Color::Rgba(RGBA::new(Some(0xff), Some(0x80), Some(0x40), Some(1.0)))
169
        );
170
2
    }
171

            
172
    // FIXME: var() - when fixing #459, see the note in the code above.  All the syntaxes
173
    // in this test function will become valid once we have full support for var().
174
    #[test]
175
2
    fn var_without_fallback_yields_error() {
176
1
        assert!(Color::parse_str("var(--foo)").is_err());
177
1
        assert!(Color::parse_str("var(--foo,)").is_err());
178
1
        assert!(Color::parse_str("var(--foo, )").is_err());
179
1
        assert!(Color::parse_str("var(--foo, this is not a color)").is_err());
180
1
        assert!(Color::parse_str("var(--foo, #112233, blah)").is_err());
181
2
    }
182

            
183
    #[test]
184
2
    fn normalizes_hue() {
185
1
        assert_eq!(normalize_hue(0.0), 0.0);
186
1
        assert_eq!(normalize_hue(360.0), 0.0);
187
1
        assert_eq!(normalize_hue(90.0), 0.25);
188
1
        assert_eq!(normalize_hue(-90.0), 0.75);
189
1
        assert_eq!(normalize_hue(450.0), 0.25); // 360 + 90 degrees
190
1
        assert_eq!(normalize_hue(-450.0), 0.75);
191
2
    }
192

            
193
    // Bug #1117
194
    #[test]
195
2
    fn large_hue_value() {
196
1
        let _ = color_to_rgba(&Color::parse_str("hsla(70000000000000,4%,10%,.2)").unwrap());
197
2
    }
198
}