csscolorparser/
parser.rs

1use std::{error, fmt};
2
3use crate::utils::remap;
4use crate::Color;
5
6#[cfg(feature = "named-colors")]
7use crate::NAMED_COLORS;
8
9/// An error which can be returned when parsing a CSS color string.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum ParseColorError {
12    /// A CSS color string was invalid hex format.
13    InvalidHex,
14    /// A CSS color string was invalid rgb format.
15    InvalidRgb,
16    /// A CSS color string was invalid hsl format.
17    InvalidHsl,
18    /// A CSS color string was invalid hwb format.
19    InvalidHwb,
20    /// A CSS color string was invalid hsv format.
21    InvalidHsv,
22    /// A CSS color string was invalid lab format.
23    #[cfg(feature = "lab")]
24    InvalidLab,
25    /// A CSS color string was invalid lch format.
26    #[cfg(feature = "lab")]
27    InvalidLch,
28    /// A CSS color string was invalid oklab format.
29    InvalidOklab,
30    /// A CSS color string was invalid oklch format.
31    InvalidOklch,
32    /// A CSS color string was invalid color function.
33    InvalidFunction,
34    /// A CSS color string was invalid unknown format.
35    InvalidUnknown,
36}
37
38impl fmt::Display for ParseColorError {
39    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40        match *self {
41            Self::InvalidHex => f.write_str("invalid hex format"),
42            Self::InvalidRgb => f.write_str("invalid rgb format"),
43            Self::InvalidHsl => f.write_str("invalid hsl format"),
44            Self::InvalidHwb => f.write_str("invalid hwb format"),
45            Self::InvalidHsv => f.write_str("invalid hsv format"),
46            #[cfg(feature = "lab")]
47            Self::InvalidLab => f.write_str("invalid lab format"),
48            #[cfg(feature = "lab")]
49            Self::InvalidLch => f.write_str("invalid lch format"),
50            Self::InvalidOklab => f.write_str("invalid oklab format"),
51            Self::InvalidOklch => f.write_str("invalid oklch format"),
52            Self::InvalidFunction => f.write_str("invalid color function"),
53            Self::InvalidUnknown => f.write_str("invalid unknown format"),
54        }
55    }
56}
57
58impl error::Error for ParseColorError {}
59
60/// Parse CSS color string
61///
62/// # Examples
63///
64/// ```
65/// # use std::error::Error;
66/// # fn main() -> Result<(), Box<dyn Error>> {
67/// let c = csscolorparser::parse("#ff0")?;
68///
69/// assert_eq!(c.to_array(), [1.0, 1.0, 0.0, 1.0]);
70/// assert_eq!(c.to_rgba8(), [255, 255, 0, 255]);
71/// assert_eq!(c.to_hex_string(), "#ffff00");
72/// assert_eq!(c.to_rgb_string(), "rgb(255,255,0)");
73/// # Ok(())
74/// # }
75/// ```
76///
77/// ```
78/// # use std::error::Error;
79/// # fn main() -> Result<(), Box<dyn Error>> {
80/// let c = csscolorparser::parse("hsl(360deg,100%,50%)")?;
81///
82/// assert_eq!(c.to_array(), [1.0, 0.0, 0.0, 1.0]);
83/// assert_eq!(c.to_rgba8(), [255, 0, 0, 255]);
84/// assert_eq!(c.to_hex_string(), "#ff0000");
85/// assert_eq!(c.to_rgb_string(), "rgb(255,0,0)");
86/// # Ok(())
87/// # }
88/// ```
89pub fn parse(s: &str) -> Result<Color, ParseColorError> {
90    let s = s.trim();
91
92    if s.eq_ignore_ascii_case("transparent") {
93        return Ok(Color::new(0.0, 0.0, 0.0, 0.0));
94    }
95
96    // Hex format
97    if let Some(s) = s.strip_prefix('#') {
98        return parse_hex(s);
99    }
100
101    if let (Some(idx), Some(s)) = (s.find('('), s.strip_suffix(')')) {
102        let fname = &s[..idx].trim_end();
103        let mut params = s[idx + 1..]
104            .split(&[',', '/'])
105            .flat_map(str::split_ascii_whitespace);
106
107        let (Some(val0), Some(val1), Some(val2)) = (params.next(), params.next(), params.next())
108        else {
109            return Err(ParseColorError::InvalidFunction);
110        };
111
112        let alpha = if let Some(a) = params.next() {
113            if let Some((v, _)) = parse_percent_or_float(a) {
114                v.clamp(0.0, 1.0)
115            } else {
116                return Err(ParseColorError::InvalidFunction);
117            }
118        } else {
119            1.0
120        };
121
122        if params.next().is_some() {
123            return Err(ParseColorError::InvalidFunction);
124        }
125
126        if fname.eq_ignore_ascii_case("rgb") || fname.eq_ignore_ascii_case("rgba") {
127            if let (Some((r, r_fmt)), Some((g, g_fmt)), Some((b, b_fmt))) = (
128                // red
129                parse_percent_or_255(val0),
130                // green
131                parse_percent_or_255(val1),
132                // blue
133                parse_percent_or_255(val2),
134            ) {
135                if r_fmt == g_fmt && g_fmt == b_fmt {
136                    return Ok(Color {
137                        r: r.clamp(0.0, 1.0),
138                        g: g.clamp(0.0, 1.0),
139                        b: b.clamp(0.0, 1.0),
140                        a: alpha,
141                    });
142                }
143            }
144
145            return Err(ParseColorError::InvalidRgb);
146        } else if fname.eq_ignore_ascii_case("hsl") || fname.eq_ignore_ascii_case("hsla") {
147            if let (Some(h), Some((s, s_fmt)), Some((l, l_fmt))) = (
148                // hue
149                parse_angle(val0),
150                // saturation
151                parse_percent_or_float(val1),
152                // lightness
153                parse_percent_or_float(val2),
154            ) {
155                if s_fmt == l_fmt {
156                    return Ok(Color::from_hsla(h, s, l, alpha));
157                }
158            }
159
160            return Err(ParseColorError::InvalidHsl);
161        } else if fname.eq_ignore_ascii_case("hwb") || fname.eq_ignore_ascii_case("hwba") {
162            if let (Some(h), Some((w, w_fmt)), Some((b, b_fmt))) = (
163                // hue
164                parse_angle(val0),
165                // whiteness
166                parse_percent_or_float(val1),
167                // blackness
168                parse_percent_or_float(val2),
169            ) {
170                if w_fmt == b_fmt {
171                    return Ok(Color::from_hwba(h, w, b, alpha));
172                }
173            }
174
175            return Err(ParseColorError::InvalidHwb);
176        } else if fname.eq_ignore_ascii_case("hsv") || fname.eq_ignore_ascii_case("hsva") {
177            if let (Some(h), Some((s, s_fmt)), Some((v, v_fmt))) = (
178                // hue
179                parse_angle(val0),
180                // saturation
181                parse_percent_or_float(val1),
182                // value
183                parse_percent_or_float(val2),
184            ) {
185                if s_fmt == v_fmt {
186                    return Ok(Color::from_hsva(h, s, v, alpha));
187                }
188            }
189
190            return Err(ParseColorError::InvalidHsv);
191        } else if fname.eq_ignore_ascii_case("lab") {
192            #[cfg(feature = "lab")]
193            if let (Some((l, l_fmt)), Some((a, a_fmt)), Some((b, b_fmt))) = (
194                // lightness
195                parse_percent_or_float(val0),
196                // a
197                parse_percent_or_float(val1),
198                // b
199                parse_percent_or_float(val2),
200            ) {
201                let l = if l_fmt { l * 100.0 } else { l };
202                let a = if a_fmt {
203                    remap(a, -1.0, 1.0, -125.0, 125.0)
204                } else {
205                    a
206                };
207                let b = if b_fmt {
208                    remap(b, -1.0, 1.0, -125.0, 125.0)
209                } else {
210                    b
211                };
212                return Ok(Color::from_laba(l.max(0.0), a, b, alpha));
213            } else {
214                return Err(ParseColorError::InvalidLab);
215            }
216        } else if fname.eq_ignore_ascii_case("lch") {
217            #[cfg(feature = "lab")]
218            if let (Some((l, l_fmt)), Some((c, c_fmt)), Some(h)) = (
219                // lightness
220                parse_percent_or_float(val0),
221                // chroma
222                parse_percent_or_float(val1),
223                // hue
224                parse_angle(val2),
225            ) {
226                let l = if l_fmt { l * 100.0 } else { l };
227                let c = if c_fmt { c * 150.0 } else { c };
228                return Ok(Color::from_lcha(
229                    l.max(0.0),
230                    c.max(0.0),
231                    h.to_radians(),
232                    alpha,
233                ));
234            } else {
235                return Err(ParseColorError::InvalidLch);
236            }
237        } else if fname.eq_ignore_ascii_case("oklab") {
238            if let (Some((l, _)), Some((a, a_fmt)), Some((b, b_fmt))) = (
239                // lightness
240                parse_percent_or_float(val0),
241                // a
242                parse_percent_or_float(val1),
243                // b
244                parse_percent_or_float(val2),
245            ) {
246                let a = if a_fmt {
247                    remap(a, -1.0, 1.0, -0.4, 0.4)
248                } else {
249                    a
250                };
251                let b = if b_fmt {
252                    remap(b, -1.0, 1.0, -0.4, 0.4)
253                } else {
254                    b
255                };
256                return Ok(Color::from_oklaba(l.max(0.0), a, b, alpha));
257            }
258
259            return Err(ParseColorError::InvalidOklab);
260        } else if fname.eq_ignore_ascii_case("oklch") {
261            if let (Some((l, _)), Some((c, c_fmt)), Some(h)) = (
262                // lightness
263                parse_percent_or_float(val0),
264                // chroma
265                parse_percent_or_float(val1),
266                // hue
267                parse_angle(val2),
268            ) {
269                let c = if c_fmt { c * 0.4 } else { c };
270                return Ok(Color::from_oklcha(
271                    l.max(0.0),
272                    c.max(0.0),
273                    h.to_radians(),
274                    alpha,
275                ));
276            }
277
278            return Err(ParseColorError::InvalidOklch);
279        }
280
281        return Err(ParseColorError::InvalidFunction);
282    }
283
284    // Hex format without prefix '#'
285    if let Ok(c) = parse_hex(s) {
286        return Ok(c);
287    }
288
289    // Named colors
290    #[cfg(feature = "named-colors")]
291    if s.len() > 2 && s.len() < 21 {
292        let s = s.to_ascii_lowercase();
293        if let Some([r, g, b]) = NAMED_COLORS.get(&s) {
294            return Ok(Color::from_rgba8(*r, *g, *b, 255));
295        }
296    }
297
298    Err(ParseColorError::InvalidUnknown)
299}
300
301fn parse_hex(s: &str) -> Result<Color, ParseColorError> {
302    if !s.is_ascii() {
303        return Err(ParseColorError::InvalidHex);
304    }
305
306    let n = s.len();
307
308    fn parse_single_digit(digit: &str) -> Result<u8, ParseColorError> {
309        u8::from_str_radix(digit, 16)
310            .map(|n| (n << 4) | n)
311            .map_err(|_| ParseColorError::InvalidHex)
312    }
313
314    if n == 3 || n == 4 {
315        let r = parse_single_digit(&s[0..1])?;
316        let g = parse_single_digit(&s[1..2])?;
317        let b = parse_single_digit(&s[2..3])?;
318
319        let a = if n == 4 {
320            parse_single_digit(&s[3..4])?
321        } else {
322            255
323        };
324
325        Ok(Color::from_rgba8(r, g, b, a))
326    } else if n == 6 || n == 8 {
327        let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| ParseColorError::InvalidHex)?;
328        let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| ParseColorError::InvalidHex)?;
329        let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| ParseColorError::InvalidHex)?;
330
331        let a = if n == 8 {
332            u8::from_str_radix(&s[6..8], 16).map_err(|_| ParseColorError::InvalidHex)?
333        } else {
334            255
335        };
336
337        Ok(Color::from_rgba8(r, g, b, a))
338    } else {
339        Err(ParseColorError::InvalidHex)
340    }
341}
342
343// strip suffix ignore case
344fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
345    if suffix.len() > s.len() {
346        return None;
347    }
348    let s_end = &s[s.len() - suffix.len()..];
349    if s_end.eq_ignore_ascii_case(suffix) {
350        Some(&s[..s.len() - suffix.len()])
351    } else {
352        None
353    }
354}
355
356fn parse_percent_or_float(s: &str) -> Option<(f32, bool)> {
357    s.strip_suffix('%')
358        .and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true)))
359        .or_else(|| s.parse().ok().map(|t| (t, false)))
360}
361
362fn parse_percent_or_255(s: &str) -> Option<(f32, bool)> {
363    s.strip_suffix('%')
364        .and_then(|s| s.parse().ok().map(|t: f32| (t / 100.0, true)))
365        .or_else(|| s.parse().ok().map(|t: f32| (t / 255.0, false)))
366}
367
368fn parse_angle(s: &str) -> Option<f32> {
369    strip_suffix(s, "deg")
370        .and_then(|s| s.parse().ok())
371        .or_else(|| {
372            strip_suffix(s, "grad")
373                .and_then(|s| s.parse().ok())
374                .map(|t: f32| t * 360.0 / 400.0)
375        })
376        .or_else(|| {
377            strip_suffix(s, "rad")
378                .and_then(|s| s.parse().ok())
379                .map(|t: f32| t.to_degrees())
380        })
381        .or_else(|| {
382            strip_suffix(s, "turn")
383                .and_then(|s| s.parse().ok())
384                .map(|t: f32| t * 360.0)
385        })
386        .or_else(|| s.parse().ok())
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_strip_suffix() {
395        assert_eq!(strip_suffix("45deg", "deg"), Some("45"));
396        assert_eq!(strip_suffix("90DEG", "deg"), Some("90"));
397        assert_eq!(strip_suffix("0.25turn", "turn"), Some("0.25"));
398        assert_eq!(strip_suffix("1.0Turn", "turn"), Some("1.0"));
399
400        assert_eq!(strip_suffix("", "deg"), None);
401        assert_eq!(strip_suffix("90", "deg"), None);
402    }
403
404    #[test]
405    fn test_parse_percent_or_float() {
406        let test_data = [
407            ("0%", Some((0.0, true))),
408            ("100%", Some((1.0, true))),
409            ("50%", Some((0.5, true))),
410            ("0", Some((0.0, false))),
411            ("1", Some((1.0, false))),
412            ("0.5", Some((0.5, false))),
413            ("100.0", Some((100.0, false))),
414            ("-23.7", Some((-23.7, false))),
415            ("%", None),
416            ("1x", None),
417        ];
418        for (s, expected) in test_data {
419            assert_eq!(parse_percent_or_float(s), expected);
420        }
421    }
422
423    #[test]
424    fn test_parse_percent_or_255() {
425        let test_data = [
426            ("0%", Some((0.0, true))),
427            ("100%", Some((1.0, true))),
428            ("50%", Some((0.5, true))),
429            ("-100%", Some((-1.0, true))),
430            ("0", Some((0.0, false))),
431            ("255", Some((1.0, false))),
432            ("127.5", Some((0.5, false))),
433            ("%", None),
434            ("255x", None),
435        ];
436        for (s, expected) in test_data {
437            assert_eq!(parse_percent_or_255(s), expected);
438        }
439    }
440
441    #[test]
442    fn test_parse_angle() {
443        let test_data = [
444            ("360", Some(360.0)),
445            ("127.356", Some(127.356)),
446            ("+120deg", Some(120.0)),
447            ("90deg", Some(90.0)),
448            ("-127deg", Some(-127.0)),
449            ("100grad", Some(90.0)),
450            ("1.5707963267948966rad", Some(90.0)),
451            ("0.25turn", Some(90.0)),
452            ("-0.25turn", Some(-90.0)),
453            ("O", None),
454            ("Odeg", None),
455            ("rad", None),
456        ];
457        for (s, expected) in test_data {
458            assert_eq!(parse_angle(s), expected);
459        }
460    }
461
462    #[test]
463    fn test_parse_hex() {
464        // case-insensitive tests
465        macro_rules! cmp {
466            ($a:expr, $b:expr) => {
467                assert_eq!(
468                    parse_hex($a).unwrap().to_rgba8(),
469                    parse_hex($b).unwrap().to_rgba8()
470                );
471            };
472        }
473        cmp!("abc", "ABC");
474        cmp!("DeF", "dEf");
475        cmp!("f0eB", "F0Eb");
476        cmp!("abcdef", "ABCDEF");
477        cmp!("Ff03E0cB", "fF03e0Cb");
478    }
479}