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" => 180.0 / f64::consts::PI,
321                    "grad" => 0.9,
322                    "turn" => 360.0,
323                    _ => return Err(ParseError::UnknownAngleDimension),
324                };
325                Ok(Some(n * scale))
326            }
327            _ => Err(ParseError::UnknownAngle),
328        }
329    }
330
331    fn optional_comma(&mut self, comma: bool) -> Result<(), ParseError> {
332        self.ws();
333        if comma && !self.ch(b',') {
334            Err(ParseError::ExpectedComma)
335        } else {
336            Ok(())
337        }
338    }
339
340    fn rgb(&mut self) -> Result<DynamicColor, ParseError> {
341        if !self.raw_ch(b'(') {
342            return Err(ParseError::ExpectedArguments);
343        }
344        // TODO: in legacy mode, be stricter about not mixing numbers
345        // and percentages, and disallowing "none"
346        let r = self
347            .scaled_component(1. / 255., 0.01)?
348            .map(|x| x.clamp(0., 1.));
349        self.ws();
350        let comma = self.ch(b',');
351        let mode = if comma { Mode::Legacy } else { Mode::Modern };
352        let g = self
353            .scaled_component(1. / 255., 0.01)?
354            .map(|x| x.clamp(0., 1.));
355        self.optional_comma(comma)?;
356        let b = self
357            .scaled_component(1. / 255., 0.01)?
358            .map(|x| x.clamp(0., 1.));
359        let alpha = self.alpha(mode)?;
360        self.ws();
361        if !self.ch(b')') {
362            return Err(ParseError::ExpectedClosingParenthesis);
363        }
364        Ok(color_from_components([r, g, b, alpha], ColorSpaceTag::Srgb))
365    }
366
367    /// Read a slash separator and an alpha value.
368    ///
369    /// The value may be either number or a percentage.
370    ///
371    /// The alpha value defaults to `1.0` if not present. The value will be clamped
372    /// to the range [0, 1].
373    ///
374    /// If the value is `"none"`, then `Ok(None)` will be returned.
375    ///
376    /// The separator will be a `'/'` in modern mode and a `','` in legacy mode.
377    /// If no separator is present, then the default value will be returned.
378    ///
379    /// Reference: ยง 4.2 of CSS Color 4 spec.
380    fn alpha(&mut self, mode: Mode) -> Result<Option<f64>, ParseError> {
381        self.ws();
382        if self.ch(mode.alpha_separator()) {
383            Ok(self.scaled_component(1., 0.01)?.map(|a| a.clamp(0., 1.)))
384        } else {
385            Ok(Some(1.0))
386        }
387    }
388
389    fn lab(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result<DynamicColor, ParseError> {
390        if !self.raw_ch(b'(') {
391            return Err(ParseError::ExpectedArguments);
392        }
393        let l = self
394            .scaled_component(1., 0.01 * lmax)?
395            .map(|x| x.clamp(0., lmax));
396        let a = self.scaled_component(1., c)?;
397        let b = self.scaled_component(1., c)?;
398        let alpha = self.alpha(Mode::Modern)?;
399        self.ws();
400        if !self.ch(b')') {
401            return Err(ParseError::ExpectedClosingParenthesis);
402        }
403        Ok(color_from_components([l, a, b, alpha], tag))
404    }
405
406    fn lch(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result<DynamicColor, ParseError> {
407        if !self.raw_ch(b'(') {
408            return Err(ParseError::ExpectedArguments);
409        }
410        let l = self
411            .scaled_component(1., 0.01 * lmax)?
412            .map(|x| x.clamp(0., lmax));
413        let c = self.scaled_component(1., c)?.map(|x| x.max(0.));
414        let h = self.angle()?;
415        let alpha = self.alpha(Mode::Modern)?;
416        self.ws();
417        if !self.ch(b')') {
418            return Err(ParseError::ExpectedClosingParenthesis);
419        }
420        Ok(color_from_components([l, c, h, alpha], tag))
421    }
422
423    fn hsl(&mut self) -> Result<DynamicColor, ParseError> {
424        if !self.raw_ch(b'(') {
425            return Err(ParseError::ExpectedArguments);
426        }
427        let h = self.angle()?;
428        let comma = self.ch(b',');
429        let mode = if comma { Mode::Legacy } else { Mode::Modern };
430        let s = self.scaled_component(1., 1.)?.map(|x| x.max(0.));
431        self.optional_comma(comma)?;
432        let l = self.scaled_component(1., 1.)?;
433        let alpha = self.alpha(mode)?;
434        self.ws();
435        if !self.ch(b')') {
436            return Err(ParseError::ExpectedClosingParenthesis);
437        }
438        Ok(color_from_components([h, s, l, alpha], ColorSpaceTag::Hsl))
439    }
440
441    fn hwb(&mut self) -> Result<DynamicColor, ParseError> {
442        if !self.raw_ch(b'(') {
443            return Err(ParseError::ExpectedArguments);
444        }
445        let h = self.angle()?;
446        let w = self.scaled_component(1., 1.)?;
447        let b = self.scaled_component(1., 1.)?;
448        let alpha = self.alpha(Mode::Modern)?;
449        self.ws();
450        if !self.ch(b')') {
451            return Err(ParseError::ExpectedClosingParenthesis);
452        }
453        Ok(color_from_components([h, w, b, alpha], ColorSpaceTag::Hwb))
454    }
455
456    fn color(&mut self) -> Result<DynamicColor, ParseError> {
457        if !self.raw_ch(b'(') {
458            return Err(ParseError::ExpectedArguments);
459        }
460        self.ws();
461        let Some(id) = self.ident() else {
462            return Err(ParseError::ExpectedColorSpaceIdentifier);
463        };
464        let mut buf = [0; LOWERCASE_BUF_SIZE];
465        let id_lc = make_lowercase(id, &mut buf);
466        let cs = match id_lc {
467            "srgb" => ColorSpaceTag::Srgb,
468            "srgb-linear" => ColorSpaceTag::LinearSrgb,
469            "display-p3" => ColorSpaceTag::DisplayP3,
470            "a98-rgb" => ColorSpaceTag::A98Rgb,
471            "prophoto-rgb" => ColorSpaceTag::ProphotoRgb,
472            "rec2020" => ColorSpaceTag::Rec2020,
473            "xyz-d50" => ColorSpaceTag::XyzD50,
474            "xyz" | "xyz-d65" => ColorSpaceTag::XyzD65,
475            _ => return Err(ParseError::UnknownColorSpace),
476        };
477        let r = self.scaled_component(1., 0.01)?;
478        let g = self.scaled_component(1., 0.01)?;
479        let b = self.scaled_component(1., 0.01)?;
480        let alpha = self.alpha(Mode::Modern)?;
481        self.ws();
482        if !self.ch(b')') {
483            return Err(ParseError::ExpectedClosingParenthesis);
484        }
485        Ok(color_from_components([r, g, b, alpha], cs))
486    }
487}
488
489/// Parse a color string prefix in CSS syntax into a color.
490///
491/// Returns the byte offset of the unparsed remainder of the string and the parsed color. See also
492/// [`parse_color`].
493///
494/// # Errors
495///
496/// Tries to return a suitable error for any invalid string, but may be
497/// a little lax on some details.
498pub fn parse_color_prefix(s: &str) -> Result<(usize, DynamicColor), ParseError> {
499    #[inline]
500    fn set_from_named_color_space(mut color: DynamicColor) -> DynamicColor {
501        color.flags.set_named_color_space();
502        color
503    }
504
505    if let Some(stripped) = s.strip_prefix('#') {
506        let (ix, channels) = get_4bit_hex_channels(stripped)?;
507        let color = color_from_4bit_hex(channels);
508        // Hex colors are seen as if they are generated from the named `rgb()` color space
509        // function.
510        let mut color = DynamicColor::from_alpha_color(color);
511        color.flags.set_named_color_space();
512        return Ok((ix + 1, color));
513    }
514    let mut parser = Parser::new(s);
515    if let Some(id) = parser.ident() {
516        let mut buf = [0; LOWERCASE_BUF_SIZE];
517        let id_lc = make_lowercase(id, &mut buf);
518        let color = match id_lc {
519            "rgb" | "rgba" => parser.rgb().map(set_from_named_color_space),
520            "lab" => parser
521                .lab(100.0, 1.25, ColorSpaceTag::Lab)
522                .map(set_from_named_color_space),
523            "lch" => parser
524                .lch(100.0, 1.25, ColorSpaceTag::Lch)
525                .map(set_from_named_color_space),
526            "oklab" => parser
527                .lab(1.0, 0.004, ColorSpaceTag::Oklab)
528                .map(set_from_named_color_space),
529            "oklch" => parser
530                .lch(1.0, 0.004, ColorSpaceTag::Oklch)
531                .map(set_from_named_color_space),
532            "hsl" | "hsla" => parser.hsl().map(set_from_named_color_space),
533            "hwb" => parser.hwb().map(set_from_named_color_space),
534            "color" => parser.color(),
535            _ => {
536                if let Some(ix) = crate::x11_colors::lookup_palette_index(id_lc) {
537                    let [r, g, b, a] = crate::x11_colors::COLORS[ix];
538                    let mut color =
539                        DynamicColor::from_alpha_color(AlphaColor::from_rgba8(r, g, b, a));
540                    color.flags.set_named_color(ix);
541                    Ok(color)
542                } else {
543                    Err(ParseError::UnknownColorIdentifier)
544                }
545            }
546        }?;
547
548        Ok((parser.ix, color))
549    } else {
550        Err(ParseError::UnknownColorSyntax)
551    }
552}
553
554/// Parse a color string in CSS syntax into a color.
555///
556/// This parses the entire string; trailing characters cause an
557/// [`ExpectedEndOfString`](ParseError::ExpectedEndOfString) parse error. Leading and trailing
558/// whitespace are ignored. See also [`parse_color_prefix`].
559///
560/// # Errors
561///
562/// Tries to return a suitable error for any invalid string, but may be
563/// a little lax on some details.
564pub fn parse_color(s: &str) -> Result<DynamicColor, ParseError> {
565    let s = s.trim();
566    let (ix, color) = parse_color_prefix(s)?;
567
568    if ix == s.len() {
569        Ok(color)
570    } else {
571        Err(ParseError::ExpectedEndOfString)
572    }
573}
574
575impl FromStr for DynamicColor {
576    type Err = ParseError;
577
578    fn from_str(s: &str) -> Result<Self, Self::Err> {
579        parse_color(s)
580    }
581}
582
583impl<CS: ColorSpace> FromStr for AlphaColor<CS> {
584    type Err = ParseError;
585
586    fn from_str(s: &str) -> Result<Self, Self::Err> {
587        parse_color(s).map(DynamicColor::to_alpha_color)
588    }
589}
590
591impl<CS: ColorSpace> FromStr for OpaqueColor<CS> {
592    type Err = ParseError;
593
594    fn from_str(s: &str) -> Result<Self, Self::Err> {
595        parse_color(s)
596            .map(DynamicColor::to_alpha_color)
597            .map(AlphaColor::discard_alpha)
598    }
599}
600
601impl<CS: ColorSpace> FromStr for PremulColor<CS> {
602    type Err = ParseError;
603
604    fn from_str(s: &str) -> Result<Self, Self::Err> {
605        parse_color(s)
606            .map(DynamicColor::to_alpha_color)
607            .map(AlphaColor::premultiply)
608    }
609}
610
611/// Parse 4-bit color channels from a hex-encoded string.
612///
613/// Returns the parsed channels and the byte offset to the remainder of the string (i.e., the
614/// number of hex characters parsed).
615const fn get_4bit_hex_channels(hex_str: &str) -> Result<(usize, [u8; 8]), ParseError> {
616    let mut hex = [0; 8];
617
618    let mut i = 0;
619    while i < 8 && i < hex_str.len() {
620        if let Ok(h) = hex_from_ascii_byte(hex_str.as_bytes()[i]) {
621            hex[i] = h;
622            i += 1;
623        } else {
624            break;
625        }
626    }
627
628    let four_bit_channels = match i {
629        3 => [hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], 15, 15],
630        4 => [
631            hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], hex[3], hex[3],
632        ],
633        6 => [hex[0], hex[1], hex[2], hex[3], hex[4], hex[5], 15, 15],
634        8 => hex,
635        _ => return Err(ParseError::WrongNumberOfHexDigits),
636    };
637
638    Ok((i, four_bit_channels))
639}
640
641const fn hex_from_ascii_byte(b: u8) -> Result<u8, ()> {
642    match b {
643        b'0'..=b'9' => Ok(b - b'0'),
644        b'A'..=b'F' => Ok(b - b'A' + 10),
645        b'a'..=b'f' => Ok(b - b'a' + 10),
646        _ => Err(()),
647    }
648}
649
650const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor<Srgb> {
651    let [r0, r1, g0, g1, b0, b1, a0, a1] = components;
652    AlphaColor::from_rgba8(
653        (r0 << 4) | r1,
654        (g0 << 4) | g1,
655        (b0 << 4) | b1,
656        (a0 << 4) | a1,
657    )
658}
659
660impl FromStr for ColorSpaceTag {
661    type Err = ParseError;
662
663    fn from_str(s: &str) -> Result<Self, Self::Err> {
664        let mut buf = [0; LOWERCASE_BUF_SIZE];
665        match make_lowercase(s, &mut buf) {
666            "srgb" => Ok(Self::Srgb),
667            "srgb-linear" => Ok(Self::LinearSrgb),
668            "lab" => Ok(Self::Lab),
669            "lch" => Ok(Self::Lch),
670            "oklab" => Ok(Self::Oklab),
671            "oklch" => Ok(Self::Oklch),
672            "display-p3" => Ok(Self::DisplayP3),
673            "a98-rgb" => Ok(Self::A98Rgb),
674            "prophoto-rgb" => Ok(Self::ProphotoRgb),
675            "xyz-d50" => Ok(Self::XyzD50),
676            "xyz" | "xyz-d65" => Ok(Self::XyzD65),
677            _ => Err(ParseError::UnknownColorSpace),
678        }
679    }
680}
681
682const LOWERCASE_BUF_SIZE: usize = 32;
683
684/// If the string contains any uppercase characters, make a lowercase copy
685/// in the provided buffer space.
686///
687/// If anything goes wrong (including the buffer size being exceeded), return
688/// the original string.
689fn make_lowercase<'a>(s: &'a str, buf: &'a mut [u8; LOWERCASE_BUF_SIZE]) -> &'a str {
690    let len = s.len();
691    if len <= LOWERCASE_BUF_SIZE && s.as_bytes().iter().any(|c| c.is_ascii_uppercase()) {
692        buf[..len].copy_from_slice(s.as_bytes());
693        if let Ok(s_copy) = str::from_utf8_mut(&mut buf[..len]) {
694            s_copy.make_ascii_lowercase();
695            s_copy
696        } else {
697            s
698        }
699    } else {
700        s
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use crate::DynamicColor;
707
708    use super::{parse_color, parse_color_prefix, Mode, ParseError, Parser};
709
710    fn assert_close_color(c1: DynamicColor, c2: DynamicColor) {
711        const EPSILON: f32 = 1e-4;
712        assert_eq!(c1.cs, c2.cs);
713        for i in 0..4 {
714            assert!((c1.components[i] - c2.components[i]).abs() < EPSILON);
715        }
716    }
717
718    fn assert_err(c: &str, err: ParseError) {
719        assert_eq!(parse_color(c).unwrap_err(), err);
720    }
721
722    #[test]
723    fn x11_color_names() {
724        let red = parse_color("red").unwrap();
725        assert_close_color(red, parse_color("rgb(255, 0, 0)").unwrap());
726        assert_close_color(red, parse_color("\n rgb(255, 0, 0)\t ").unwrap());
727        let lgy = parse_color("lightgoldenrodyellow").unwrap();
728        assert_close_color(lgy, parse_color("rgb(250, 250, 210)").unwrap());
729        let transparent = parse_color("transparent").unwrap();
730        assert_close_color(transparent, parse_color("rgba(0, 0, 0, 0)").unwrap());
731    }
732
733    #[test]
734    fn hex() {
735        let red = parse_color("red").unwrap();
736        assert_close_color(red, parse_color("#f00").unwrap());
737        assert_close_color(red, parse_color("#f00f").unwrap());
738        assert_close_color(red, parse_color("#ff0000ff").unwrap());
739        assert_eq!(
740            parse_color("#f00fa").unwrap_err(),
741            ParseError::WrongNumberOfHexDigits
742        );
743    }
744
745    #[test]
746    fn consume_string() {
747        assert_eq!(
748            parse_color("#ff0000ffa").unwrap_err(),
749            ParseError::ExpectedEndOfString
750        );
751        assert_eq!(
752            parse_color("rgba(255, 100, 0, 1)a").unwrap_err(),
753            ParseError::ExpectedEndOfString
754        );
755    }
756
757    #[test]
758    fn prefix() {
759        for (color, trailing) in [
760            ("color(rec2020 0.2 0.3 0.4 / 0.85)trailing", "trailing"),
761            ("color(rec2020 0.2 0.3 0.4 / 0.85) ", " "),
762            ("color(rec2020 0.2 0.3 0.4 / 0.85)", ""),
763            ("red\0", "\0"),
764            ("#ffftrailing", "trailing"),
765            ("#fffffftr", "tr"),
766        ] {
767            assert_eq!(&color[parse_color_prefix(color).unwrap().0..], trailing);
768        }
769    }
770
771    #[test]
772    fn consume_comments() {
773        for (s, remaining) in [
774            ("/* abc */ def", " def"),
775            ("/* *//* */abc", "abc"),
776            ("/* /* */abc", "abc"),
777        ] {
778            let mut parser = Parser::new(s);
779            assert!(parser.consume_comments().is_ok());
780            assert_eq!(&parser.s[parser.ix..], remaining);
781        }
782    }
783
784    #[test]
785    fn alpha() {
786        for (alpha, expected, mode) in [
787            (", 10%", Ok(Some(0.1)), Mode::Legacy),
788            ("/ 0.25", Ok(Some(0.25)), Mode::Modern),
789            ("/ -0.3", Ok(Some(0.)), Mode::Modern),
790            ("/ 110%", Ok(Some(1.)), Mode::Modern),
791            ("", Ok(Some(1.)), Mode::Legacy),
792            ("/ none", Ok(None), Mode::Modern),
793        ] {
794            let mut parser = Parser::new(alpha);
795            let result = parser.alpha(mode);
796            assert_eq!(result, expected,
797                "Failed parsing specified alpha `{alpha}`. Expected: `{expected:?}`. Got: `{result:?}`.");
798        }
799    }
800
801    #[test]
802    fn angles() {
803        for (angle, expected) in [
804            ("90deg", 90.),
805            ("1.5707963rad", 90.),
806            ("100grad", 90.),
807            ("0.25turn", 90.),
808        ] {
809            let mut parser = Parser::new(angle);
810            let result = parser.angle().unwrap().unwrap();
811            assert!((result - expected).abs() < 1e-4,
812                    "Failed parsing specified angle `{angle}`. Expected: `{expected:?}`. Got: `{result:?}`.");
813        }
814
815        {
816            let mut parser = Parser::new("none");
817            assert_eq!(parser.angle().unwrap(), None);
818        }
819
820        assert_err(
821            "hwb(1turns 20% 30% / 50%)",
822            ParseError::UnknownAngleDimension,
823        );
824    }
825
826    #[test]
827    fn case_insensitive() {
828        for (c1, c2) in [
829            ("red", "ReD"),
830            ("lightgoldenrodyellow", "LightGoldenRodYellow"),
831            ("rgb(102, 51, 153)", "RGB(102, 51, 153)"),
832            (
833                "color(rec2020 0.2 0.3 0.4 / 0.85)",
834                "CoLoR(ReC2020 0.2 0.3 0.4 / 0.85)",
835            ),
836            ("hwb(120deg 30% 50%)", "HwB(120DeG 30% 50%)"),
837            ("hsl(none none none)", "HSL(NONE NONE NONE)"),
838        ] {
839            assert_close_color(parse_color(c1).unwrap(), parse_color(c2).unwrap());
840        }
841    }
842}