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