1use 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#[derive(Clone, Debug, Eq, PartialEq)]
22#[non_exhaustive]
23pub enum ParseError {
24 UnclosedComment,
26 UnknownAngleDimension,
28 UnknownAngle,
30 UnknownColorComponent,
32 UnknownColorIdentifier,
34 UnknownColorSpace,
36 UnknownColorSyntax,
38 ExpectedArguments,
40 ExpectedClosingParenthesis,
42 ExpectedColorSpaceIdentifier,
44 ExpectedComma,
46 ExpectedEndOfString,
48 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#[derive(Debug, Clone)]
83enum Value<'a> {
84 Symbol(&'a str),
85 Number(f64),
86 Percent(f64),
87 Dimension(f64, &'a str),
88}
89
90#[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 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 if let Ok(value) = tail[..i].parse() {
194 self.ix += i;
195 return Some(value);
196 }
197 }
198 None
199 }
200
201 fn ident(&mut self) -> Option<&'a str> {
206 let tail = &self.s[self.ix..];
208 let i_init = 0; 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 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 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 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 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 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 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
493pub 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 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
558pub 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
615const 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
688fn 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}