color/
parse.rs

1// Copyright 2024 the Color Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Parse CSS4 color
5
6use core::error::Error;
7use core::f64;
8use core::fmt;
9use core::str;
10use core::str::FromStr;
11
12use crate::{
13    AlphaColor, ColorSpace, ColorSpaceTag, DynamicColor, Flags, Missing, OpaqueColor, PremulColor,
14    Srgb,
15};
16
17// TODO: maybe include string offset
18/// Error type for parse errors.
19///
20/// Discussion question: should it also contain a string offset?
21#[derive(Clone, Debug, Eq, PartialEq)]
22#[non_exhaustive]
23pub enum ParseError {
24    /// Unclosed comment
25    UnclosedComment,
26    /// Unknown angle dimension
27    UnknownAngleDimension,
28    /// Unknown angle
29    UnknownAngle,
30    /// Unknown color component
31    UnknownColorComponent,
32    /// Unknown color identifier
33    UnknownColorIdentifier,
34    /// Unknown color space
35    UnknownColorSpace,
36    /// Unknown color syntax
37    UnknownColorSyntax,
38    /// Expected arguments
39    ExpectedArguments,
40    /// Expected closing parenthesis
41    ExpectedClosingParenthesis,
42    /// Expected color space identifier
43    ExpectedColorSpaceIdentifier,
44    /// Expected comma
45    ExpectedComma,
46    /// Expected end of string
47    ExpectedEndOfString,
48    /// Wrong number of hex digits
49    WrongNumberOfHexDigits,
50}
51
52impl Error for ParseError {}
53
54impl fmt::Display for ParseError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        let msg = match *self {
57            Self::UnclosedComment => "unclosed comment",
58            Self::UnknownAngleDimension => "unknown angle dimension",
59            Self::UnknownAngle => "unknown angle",
60            Self::UnknownColorComponent => "unknown color component",
61            Self::UnknownColorIdentifier => "unknown color identifier",
62            Self::UnknownColorSpace => "unknown color space",
63            Self::UnknownColorSyntax => "unknown color syntax",
64            Self::ExpectedArguments => "expected arguments",
65            Self::ExpectedClosingParenthesis => "expected closing parenthesis",
66            Self::ExpectedColorSpaceIdentifier => "expected color space identifier",
67            Self::ExpectedComma => "expected comma",
68            Self::ExpectedEndOfString => "expected end of string",
69            Self::WrongNumberOfHexDigits => "wrong number of hex digits",
70        };
71        f.write_str(msg)
72    }
73}
74
75#[derive(Default)]
76struct Parser<'a> {
77    s: &'a str,
78    ix: usize,
79}
80
81/// A parsed value.
82#[derive(Debug, Clone)]
83enum Value<'a> {
84    Symbol(&'a str),
85    Number(f64),
86    Percent(f64),
87    Dimension(f64, &'a str),
88}
89
90/// Whether or not we are parsing modern or legacy mode syntax.
91#[derive(Clone, Copy, Debug, PartialEq)]
92enum Mode {
93    Legacy,
94    Modern,
95}
96
97impl Mode {
98    fn alpha_separator(self) -> u8 {
99        match self {
100            Self::Legacy => b',',
101            Self::Modern => b'/',
102        }
103    }
104}
105
106#[expect(
107    clippy::cast_possible_truncation,
108    reason = "deliberate choice of f32 for colors"
109)]
110fn color_from_components(components: [Option<f64>; 4], cs: ColorSpaceTag) -> DynamicColor {
111    let mut missing = Missing::default();
112    for (i, component) in components.iter().enumerate() {
113        if component.is_none() {
114            missing.insert(i);
115        }
116    }
117    DynamicColor {
118        cs,
119        flags: Flags::from_missing(missing),
120        components: components.map(|x| x.unwrap_or(0.0) as f32),
121    }
122}
123
124impl<'a> Parser<'a> {
125    fn new(s: &'a str) -> Self {
126        let ix = 0;
127        Parser { s, ix }
128    }
129
130    // This will be called at the start of most tokens.
131    fn consume_comments(&mut self) -> Result<(), ParseError> {
132        while self.s[self.ix..].starts_with("/*") {
133            if let Some(i) = self.s[self.ix + 2..].find("*/") {
134                self.ix += i + 4;
135            } else {
136                return Err(ParseError::UnclosedComment);
137            }
138        }
139        Ok(())
140    }
141
142    fn number(&mut self) -> Option<f64> {
143        self.consume_comments().ok()?;
144        let tail = &self.s[self.ix..];
145        let mut i = 0;
146        let mut valid = false;
147        if matches!(tail.as_bytes().first(), Some(b'+' | b'-')) {
148            i += 1;
149        }
150        while let Some(c) = tail.as_bytes().get(i) {
151            if c.is_ascii_digit() {
152                valid = true;
153                i += 1;
154            } else {
155                break;
156            }
157        }
158        if let Some(b'.') = tail.as_bytes().get(i) {
159            if let Some(c) = tail.as_bytes().get(i + 1) {
160                if c.is_ascii_digit() {
161                    valid = true;
162                    i += 2;
163                    while let Some(c2) = tail.as_bytes().get(i) {
164                        if c2.is_ascii_digit() {
165                            i += 1;
166                        } else {
167                            break;
168                        }
169                    }
170                }
171            }
172        }
173        if matches!(tail.as_bytes().get(i), Some(b'e' | b'E')) {
174            let mut j = i + 1;
175            if matches!(tail.as_bytes().get(j), Some(b'+' | b'-')) {
176                j += 1;
177            }
178            if let Some(c) = tail.as_bytes().get(j) {
179                if c.is_ascii_digit() {
180                    i = j + 1;
181                    while let Some(c2) = tail.as_bytes().get(i) {
182                        if c2.is_ascii_digit() {
183                            i += 1;
184                        } else {
185                            break;
186                        }
187                    }
188                }
189            }
190        }
191        if valid {
192            // For this parse to fail would be strange, but we'll be careful.
193            if let Ok(value) = tail[..i].parse() {
194                self.ix += i;
195                return Some(value);
196            }
197        }
198        None
199    }
200
201    // Complies with ident-token production with three exceptions:
202    // Escapes are not supported.
203    // Non-ASCII characters are not supported.
204    // Result is case sensitive.
205    fn ident(&mut self) -> Option<&'a str> {
206        // This does *not* strip initial whitespace.
207        let tail = &self.s[self.ix..];
208        let i_init = 0; // This exists as a vestige for syntax like :ident
209        let mut i = i_init;
210        while i < tail.len() {
211            let b = tail.as_bytes()[i];
212            if b.is_ascii_alphabetic()
213                || b == b'_'
214                || b == b'-'
215                || ((i >= 2 || i == 1 && tail.as_bytes()[i_init] != b'-') && b.is_ascii_digit())
216            {
217                i += 1;
218            } else {
219                break;
220            }
221        }
222        // Reject '', '-', and anything starting with '--'
223        let mut j = i_init;
224        while j < i.min(i_init + 2) {
225            if tail.as_bytes()[j] == b'-' {
226                j += 1;
227            } else {
228                self.ix += i;
229                return Some(&tail[..i]);
230            }
231        }
232        None
233    }
234
235    fn ch(&mut self, ch: u8) -> bool {
236        if self.consume_comments().is_err() {
237            return false;
238        }
239        self.raw_ch(ch)
240    }
241
242    /// Attempt to read the exact ASCII character given, returning whether that character was read.
243    ///
244    /// The parser proceeds to the next character if the character was successfully read.
245    fn raw_ch(&mut self, ch: u8) -> bool {
246        debug_assert!(ch.is_ascii(), "`ch` must be an ASCII character");
247        if self.s.as_bytes().get(self.ix) == Some(&ch) {
248            self.ix += 1;
249            true
250        } else {
251            false
252        }
253    }
254
255    fn ws_one(&mut self) -> bool {
256        if self.consume_comments().is_err() {
257            return false;
258        }
259        let tail = &self.s[self.ix..];
260        let mut i = 0;
261        while let Some(&b) = tail.as_bytes().get(i) {
262            if !(b == b' ' || b == b'\t' || b == b'\r' || b == b'\n') {
263                break;
264            }
265            i += 1;
266        }
267        self.ix += i;
268        i > 0
269    }
270
271    fn ws(&mut self) -> bool {
272        if !self.ws_one() {
273            return false;
274        }
275        while self.consume_comments().is_ok() {
276            if !self.ws_one() {
277                break;
278            }
279        }
280        true
281    }
282
283    fn value(&mut self) -> Option<Value<'a>> {
284        if let Some(number) = self.number() {
285            if self.raw_ch(b'%') {
286                Some(Value::Percent(number))
287            } else if let Some(unit) = self.ident() {
288                Some(Value::Dimension(number, unit))
289            } else {
290                Some(Value::Number(number))
291            }
292        } else {
293            self.ident().map(Value::Symbol)
294        }
295    }
296
297    /// Parse a color component.
298    fn scaled_component(&mut self, scale: f64, pct_scale: f64) -> Result<Option<f64>, ParseError> {
299        self.ws();
300        let value = self.value();
301        match value {
302            Some(Value::Number(n)) => Ok(Some(n * scale)),
303            Some(Value::Percent(n)) => Ok(Some(n * pct_scale)),
304            Some(Value::Symbol(s)) if s.eq_ignore_ascii_case("none") => Ok(None),
305            _ => Err(ParseError::UnknownColorComponent),
306        }
307    }
308
309    fn angle(&mut self) -> Result<Option<f64>, ParseError> {
310        self.ws();
311        let value = self.value();
312        match value {
313            Some(Value::Number(n)) => Ok(Some(n)),
314            Some(Value::Symbol(s)) if s.eq_ignore_ascii_case("none") => Ok(None),
315            Some(Value::Dimension(n, dim)) => {
316                let mut buf = [0; LOWERCASE_BUF_SIZE];
317                let dim_lc = make_lowercase(dim, &mut buf);
318                let scale = match dim_lc {
319                    "deg" => 1.0,
320                    "rad" => {
321                        // TODO: to make doubly sure this is computed at compile-time, this can be
322                        // wrapped in a `const` block when our MSRV is 1.83 or greater.
323                        1_f64.to_degrees()
324                    }
325                    "grad" => 0.9,
326                    "turn" => 360.0,
327                    _ => return Err(ParseError::UnknownAngleDimension),
328                };
329                Ok(Some(n * scale))
330            }
331            _ => Err(ParseError::UnknownAngle),
332        }
333    }
334
335    fn optional_comma(&mut self, comma: bool) -> Result<(), ParseError> {
336        self.ws();
337        if comma && !self.ch(b',') {
338            Err(ParseError::ExpectedComma)
339        } else {
340            Ok(())
341        }
342    }
343
344    fn rgb(&mut self) -> Result<DynamicColor, ParseError> {
345        if !self.raw_ch(b'(') {
346            return Err(ParseError::ExpectedArguments);
347        }
348        // TODO: in legacy mode, be stricter about not mixing numbers
349        // and percentages, and disallowing "none"
350        let r = self
351            .scaled_component(1. / 255., 0.01)?
352            .map(|x| x.clamp(0., 1.));
353        self.ws();
354        let comma = self.ch(b',');
355        let mode = if comma { Mode::Legacy } else { Mode::Modern };
356        let g = self
357            .scaled_component(1. / 255., 0.01)?
358            .map(|x| x.clamp(0., 1.));
359        self.optional_comma(comma)?;
360        let b = self
361            .scaled_component(1. / 255., 0.01)?
362            .map(|x| x.clamp(0., 1.));
363        let alpha = self.alpha(mode)?;
364        self.ws();
365        if !self.ch(b')') {
366            return Err(ParseError::ExpectedClosingParenthesis);
367        }
368        Ok(color_from_components([r, g, b, alpha], ColorSpaceTag::Srgb))
369    }
370
371    /// Read a slash separator and an alpha value.
372    ///
373    /// The value may be either number or a percentage.
374    ///
375    /// The alpha value defaults to `1.0` if not present. The value will be clamped
376    /// to the range [0, 1].
377    ///
378    /// If the value is `"none"`, then `Ok(None)` will be returned.
379    ///
380    /// The separator will be a `'/'` in modern mode and a `','` in legacy mode.
381    /// If no separator is present, then the default value will be returned.
382    ///
383    /// Reference: ยง 4.2 of CSS Color 4 spec.
384    fn alpha(&mut self, mode: Mode) -> Result<Option<f64>, ParseError> {
385        self.ws();
386        if self.ch(mode.alpha_separator()) {
387            Ok(self.scaled_component(1., 0.01)?.map(|a| a.clamp(0., 1.)))
388        } else {
389            Ok(Some(1.0))
390        }
391    }
392
393    fn lab(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result<DynamicColor, ParseError> {
394        if !self.raw_ch(b'(') {
395            return Err(ParseError::ExpectedArguments);
396        }
397        let l = self
398            .scaled_component(1., 0.01 * lmax)?
399            .map(|x| x.clamp(0., lmax));
400        let a = self.scaled_component(1., c)?;
401        let b = self.scaled_component(1., c)?;
402        let alpha = self.alpha(Mode::Modern)?;
403        self.ws();
404        if !self.ch(b')') {
405            return Err(ParseError::ExpectedClosingParenthesis);
406        }
407        Ok(color_from_components([l, a, b, alpha], tag))
408    }
409
410    fn lch(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result<DynamicColor, ParseError> {
411        if !self.raw_ch(b'(') {
412            return Err(ParseError::ExpectedArguments);
413        }
414        let l = self
415            .scaled_component(1., 0.01 * lmax)?
416            .map(|x| x.clamp(0., lmax));
417        let c = self.scaled_component(1., c)?.map(|x| x.max(0.));
418        let h = self.angle()?;
419        let alpha = self.alpha(Mode::Modern)?;
420        self.ws();
421        if !self.ch(b')') {
422            return Err(ParseError::ExpectedClosingParenthesis);
423        }
424        Ok(color_from_components([l, c, h, alpha], tag))
425    }
426
427    fn hsl(&mut self) -> Result<DynamicColor, ParseError> {
428        if !self.raw_ch(b'(') {
429            return Err(ParseError::ExpectedArguments);
430        }
431        let h = self.angle()?;
432        let comma = self.ch(b',');
433        let mode = if comma { Mode::Legacy } else { Mode::Modern };
434        let s = self.scaled_component(1., 1.)?.map(|x| x.max(0.));
435        self.optional_comma(comma)?;
436        let l = self.scaled_component(1., 1.)?;
437        let alpha = self.alpha(mode)?;
438        self.ws();
439        if !self.ch(b')') {
440            return Err(ParseError::ExpectedClosingParenthesis);
441        }
442        Ok(color_from_components([h, s, l, alpha], ColorSpaceTag::Hsl))
443    }
444
445    fn hwb(&mut self) -> Result<DynamicColor, ParseError> {
446        if !self.raw_ch(b'(') {
447            return Err(ParseError::ExpectedArguments);
448        }
449        let h = self.angle()?;
450        let w = self.scaled_component(1., 1.)?;
451        let b = self.scaled_component(1., 1.)?;
452        let alpha = self.alpha(Mode::Modern)?;
453        self.ws();
454        if !self.ch(b')') {
455            return Err(ParseError::ExpectedClosingParenthesis);
456        }
457        Ok(color_from_components([h, w, b, alpha], ColorSpaceTag::Hwb))
458    }
459
460    fn color(&mut self) -> Result<DynamicColor, ParseError> {
461        if !self.raw_ch(b'(') {
462            return Err(ParseError::ExpectedArguments);
463        }
464        self.ws();
465        let Some(id) = self.ident() else {
466            return Err(ParseError::ExpectedColorSpaceIdentifier);
467        };
468        let mut buf = [0; LOWERCASE_BUF_SIZE];
469        let id_lc = make_lowercase(id, &mut buf);
470        let cs = match id_lc {
471            "srgb" => ColorSpaceTag::Srgb,
472            "srgb-linear" => ColorSpaceTag::LinearSrgb,
473            "display-p3" => ColorSpaceTag::DisplayP3,
474            "a98-rgb" => ColorSpaceTag::A98Rgb,
475            "prophoto-rgb" => ColorSpaceTag::ProphotoRgb,
476            "rec2020" => ColorSpaceTag::Rec2020,
477            "xyz-d50" => ColorSpaceTag::XyzD50,
478            "xyz" | "xyz-d65" => ColorSpaceTag::XyzD65,
479            _ => return Err(ParseError::UnknownColorSpace),
480        };
481        let r = self.scaled_component(1., 0.01)?;
482        let g = self.scaled_component(1., 0.01)?;
483        let b = self.scaled_component(1., 0.01)?;
484        let alpha = self.alpha(Mode::Modern)?;
485        self.ws();
486        if !self.ch(b')') {
487            return Err(ParseError::ExpectedClosingParenthesis);
488        }
489        Ok(color_from_components([r, g, b, alpha], cs))
490    }
491}
492
493/// Parse a color string prefix in CSS syntax into a color.
494///
495/// Returns the byte offset of the unparsed remainder of the string and the parsed color. See also
496/// [`parse_color`].
497///
498/// # Errors
499///
500/// Tries to return a suitable error for any invalid string, but may be
501/// a little lax on some details.
502pub fn parse_color_prefix(s: &str) -> Result<(usize, DynamicColor), ParseError> {
503    #[inline]
504    fn set_from_named_color_space(mut color: DynamicColor) -> DynamicColor {
505        color.flags.set_named_color_space();
506        color
507    }
508
509    if let Some(stripped) = s.strip_prefix('#') {
510        let (ix, channels) = get_4bit_hex_channels(stripped)?;
511        let color = color_from_4bit_hex(channels);
512        // Hex colors are seen as if they are generated from the named `rgb()` color space
513        // function.
514        let mut color = DynamicColor::from_alpha_color(color);
515        color.flags.set_named_color_space();
516        return Ok((ix + 1, color));
517    }
518    let mut parser = Parser::new(s);
519    if let Some(id) = parser.ident() {
520        let mut buf = [0; LOWERCASE_BUF_SIZE];
521        let id_lc = make_lowercase(id, &mut buf);
522        let color = match id_lc {
523            "rgb" | "rgba" => parser.rgb().map(set_from_named_color_space),
524            "lab" => parser
525                .lab(100.0, 1.25, ColorSpaceTag::Lab)
526                .map(set_from_named_color_space),
527            "lch" => parser
528                .lch(100.0, 1.25, ColorSpaceTag::Lch)
529                .map(set_from_named_color_space),
530            "oklab" => parser
531                .lab(1.0, 0.004, ColorSpaceTag::Oklab)
532                .map(set_from_named_color_space),
533            "oklch" => parser
534                .lch(1.0, 0.004, ColorSpaceTag::Oklch)
535                .map(set_from_named_color_space),
536            "hsl" | "hsla" => parser.hsl().map(set_from_named_color_space),
537            "hwb" => parser.hwb().map(set_from_named_color_space),
538            "color" => parser.color(),
539            _ => {
540                if let Some(ix) = crate::x11_colors::lookup_palette_index(id_lc) {
541                    let [r, g, b, a] = crate::x11_colors::COLORS[ix];
542                    let mut color =
543                        DynamicColor::from_alpha_color(AlphaColor::from_rgba8(r, g, b, a));
544                    color.flags.set_named_color(ix);
545                    Ok(color)
546                } else {
547                    Err(ParseError::UnknownColorIdentifier)
548                }
549            }
550        }?;
551
552        Ok((parser.ix, color))
553    } else {
554        Err(ParseError::UnknownColorSyntax)
555    }
556}
557
558/// Parse a color string in CSS syntax into a color.
559///
560/// This parses the entire string; trailing characters cause an
561/// [`ExpectedEndOfString`](ParseError::ExpectedEndOfString) parse error. Leading and trailing
562/// whitespace are ignored. See also [`parse_color_prefix`].
563///
564/// # Errors
565///
566/// Tries to return a suitable error for any invalid string, but may be
567/// a little lax on some details.
568pub fn parse_color(s: &str) -> Result<DynamicColor, ParseError> {
569    let s = s.trim();
570    let (ix, color) = parse_color_prefix(s)?;
571
572    if ix == s.len() {
573        Ok(color)
574    } else {
575        Err(ParseError::ExpectedEndOfString)
576    }
577}
578
579impl FromStr for DynamicColor {
580    type Err = ParseError;
581
582    fn from_str(s: &str) -> Result<Self, Self::Err> {
583        parse_color(s)
584    }
585}
586
587impl<CS: ColorSpace> FromStr for AlphaColor<CS> {
588    type Err = ParseError;
589
590    fn from_str(s: &str) -> Result<Self, Self::Err> {
591        parse_color(s).map(DynamicColor::to_alpha_color)
592    }
593}
594
595impl<CS: ColorSpace> FromStr for OpaqueColor<CS> {
596    type Err = ParseError;
597
598    fn from_str(s: &str) -> Result<Self, Self::Err> {
599        parse_color(s)
600            .map(DynamicColor::to_alpha_color)
601            .map(AlphaColor::discard_alpha)
602    }
603}
604
605impl<CS: ColorSpace> FromStr for PremulColor<CS> {
606    type Err = ParseError;
607
608    fn from_str(s: &str) -> Result<Self, Self::Err> {
609        parse_color(s)
610            .map(DynamicColor::to_alpha_color)
611            .map(AlphaColor::premultiply)
612    }
613}
614
615/// Parse 4-bit color channels from a hex-encoded string.
616///
617/// Returns the parsed channels and the byte offset to the remainder of the string (i.e., the
618/// number of hex characters parsed).
619const fn get_4bit_hex_channels(hex_str: &str) -> Result<(usize, [u8; 8]), ParseError> {
620    let mut hex = [0; 8];
621
622    let mut i = 0;
623    while i < 8 && i < hex_str.len() {
624        if let Ok(h) = hex_from_ascii_byte(hex_str.as_bytes()[i]) {
625            hex[i] = h;
626            i += 1;
627        } else {
628            break;
629        }
630    }
631
632    let four_bit_channels = match i {
633        3 => [hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], 15, 15],
634        4 => [
635            hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], hex[3], hex[3],
636        ],
637        6 => [hex[0], hex[1], hex[2], hex[3], hex[4], hex[5], 15, 15],
638        8 => hex,
639        _ => return Err(ParseError::WrongNumberOfHexDigits),
640    };
641
642    Ok((i, four_bit_channels))
643}
644
645const fn hex_from_ascii_byte(b: u8) -> Result<u8, ()> {
646    match b {
647        b'0'..=b'9' => Ok(b - b'0'),
648        b'A'..=b'F' => Ok(b - b'A' + 10),
649        b'a'..=b'f' => Ok(b - b'a' + 10),
650        _ => Err(()),
651    }
652}
653
654const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor<Srgb> {
655    let [r0, r1, g0, g1, b0, b1, a0, a1] = components;
656    AlphaColor::from_rgba8(
657        (r0 << 4) | r1,
658        (g0 << 4) | g1,
659        (b0 << 4) | b1,
660        (a0 << 4) | a1,
661    )
662}
663
664impl FromStr for ColorSpaceTag {
665    type Err = ParseError;
666
667    fn from_str(s: &str) -> Result<Self, Self::Err> {
668        let mut buf = [0; LOWERCASE_BUF_SIZE];
669        match make_lowercase(s, &mut buf) {
670            "srgb" => Ok(Self::Srgb),
671            "srgb-linear" => Ok(Self::LinearSrgb),
672            "lab" => Ok(Self::Lab),
673            "lch" => Ok(Self::Lch),
674            "oklab" => Ok(Self::Oklab),
675            "oklch" => Ok(Self::Oklch),
676            "display-p3" => Ok(Self::DisplayP3),
677            "a98-rgb" => Ok(Self::A98Rgb),
678            "prophoto-rgb" => Ok(Self::ProphotoRgb),
679            "xyz-d50" => Ok(Self::XyzD50),
680            "xyz" | "xyz-d65" => Ok(Self::XyzD65),
681            _ => Err(ParseError::UnknownColorSpace),
682        }
683    }
684}
685
686const LOWERCASE_BUF_SIZE: usize = 32;
687
688/// If the string contains any uppercase characters, make a lowercase copy
689/// in the provided buffer space.
690///
691/// If anything goes wrong (including the buffer size being exceeded), return
692/// the original string.
693fn make_lowercase<'a>(s: &'a str, buf: &'a mut [u8; LOWERCASE_BUF_SIZE]) -> &'a str {
694    let len = s.len();
695    if len <= LOWERCASE_BUF_SIZE && s.as_bytes().iter().any(|c| c.is_ascii_uppercase()) {
696        buf[..len].copy_from_slice(s.as_bytes());
697        if let Ok(s_copy) = str::from_utf8_mut(&mut buf[..len]) {
698            s_copy.make_ascii_lowercase();
699            s_copy
700        } else {
701            s
702        }
703    } else {
704        s
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use crate::DynamicColor;
711
712    use super::{parse_color, parse_color_prefix, Mode, ParseError, Parser};
713
714    fn assert_close_color(c1: DynamicColor, c2: DynamicColor) {
715        const EPSILON: f32 = 1e-4;
716        assert_eq!(c1.cs, c2.cs);
717        for i in 0..4 {
718            assert!((c1.components[i] - c2.components[i]).abs() < EPSILON);
719        }
720    }
721
722    fn assert_err(c: &str, err: ParseError) {
723        assert_eq!(parse_color(c).unwrap_err(), err);
724    }
725
726    #[test]
727    fn x11_color_names() {
728        let red = parse_color("red").unwrap();
729        assert_close_color(red, parse_color("rgb(255, 0, 0)").unwrap());
730        assert_close_color(red, parse_color("\n rgb(255, 0, 0)\t ").unwrap());
731        let lgy = parse_color("lightgoldenrodyellow").unwrap();
732        assert_close_color(lgy, parse_color("rgb(250, 250, 210)").unwrap());
733        let transparent = parse_color("transparent").unwrap();
734        assert_close_color(transparent, parse_color("rgba(0, 0, 0, 0)").unwrap());
735    }
736
737    #[test]
738    fn hex() {
739        let red = parse_color("red").unwrap();
740        assert_close_color(red, parse_color("#f00").unwrap());
741        assert_close_color(red, parse_color("#f00f").unwrap());
742        assert_close_color(red, parse_color("#ff0000ff").unwrap());
743        assert_eq!(
744            parse_color("#f00fa").unwrap_err(),
745            ParseError::WrongNumberOfHexDigits
746        );
747    }
748
749    #[test]
750    fn consume_string() {
751        assert_eq!(
752            parse_color("#ff0000ffa").unwrap_err(),
753            ParseError::ExpectedEndOfString
754        );
755        assert_eq!(
756            parse_color("rgba(255, 100, 0, 1)a").unwrap_err(),
757            ParseError::ExpectedEndOfString
758        );
759    }
760
761    #[test]
762    fn prefix() {
763        for (color, trailing) in [
764            ("color(rec2020 0.2 0.3 0.4 / 0.85)trailing", "trailing"),
765            ("color(rec2020 0.2 0.3 0.4 / 0.85) ", " "),
766            ("color(rec2020 0.2 0.3 0.4 / 0.85)", ""),
767            ("red\0", "\0"),
768            ("#ffftrailing", "trailing"),
769            ("#fffffftr", "tr"),
770        ] {
771            assert_eq!(&color[parse_color_prefix(color).unwrap().0..], trailing);
772        }
773    }
774
775    #[test]
776    fn consume_comments() {
777        for (s, remaining) in [
778            ("/* abc */ def", " def"),
779            ("/* *//* */abc", "abc"),
780            ("/* /* */abc", "abc"),
781        ] {
782            let mut parser = Parser::new(s);
783            assert!(parser.consume_comments().is_ok());
784            assert_eq!(&parser.s[parser.ix..], remaining);
785        }
786    }
787
788    #[test]
789    fn alpha() {
790        for (alpha, expected, mode) in [
791            (", 10%", Ok(Some(0.1)), Mode::Legacy),
792            ("/ 0.25", Ok(Some(0.25)), Mode::Modern),
793            ("/ -0.3", Ok(Some(0.)), Mode::Modern),
794            ("/ 110%", Ok(Some(1.)), Mode::Modern),
795            ("", Ok(Some(1.)), Mode::Legacy),
796            ("/ none", Ok(None), Mode::Modern),
797        ] {
798            let mut parser = Parser::new(alpha);
799            let result = parser.alpha(mode);
800            assert_eq!(result, expected,
801                "Failed parsing specified alpha `{alpha}`. Expected: `{expected:?}`. Got: `{result:?}`.");
802        }
803    }
804
805    #[test]
806    fn angles() {
807        for (angle, expected) in [
808            ("90deg", 90.),
809            ("1.5707963rad", 90.),
810            ("100grad", 90.),
811            ("0.25turn", 90.),
812        ] {
813            let mut parser = Parser::new(angle);
814            let result = parser.angle().unwrap().unwrap();
815            assert!((result - expected).abs() < 1e-4,
816                    "Failed parsing specified angle `{angle}`. Expected: `{expected:?}`. Got: `{result:?}`.");
817        }
818
819        {
820            let mut parser = Parser::new("none");
821            assert_eq!(parser.angle().unwrap(), None);
822        }
823
824        assert_err(
825            "hwb(1turns 20% 30% / 50%)",
826            ParseError::UnknownAngleDimension,
827        );
828    }
829
830    #[test]
831    fn case_insensitive() {
832        for (c1, c2) in [
833            ("red", "ReD"),
834            ("lightgoldenrodyellow", "LightGoldenRodYellow"),
835            ("rgb(102, 51, 153)", "RGB(102, 51, 153)"),
836            (
837                "color(rec2020 0.2 0.3 0.4 / 0.85)",
838                "CoLoR(ReC2020 0.2 0.3 0.4 / 0.85)",
839            ),
840            ("hwb(120deg 30% 50%)", "HwB(120DeG 30% 50%)"),
841            ("hsl(none none none)", "HSL(NONE NONE NONE)"),
842        ] {
843            assert_close_color(parse_color(c1).unwrap(), parse_color(c2).unwrap());
844        }
845    }
846}